Overview
Ruby modules serve as containers for methods, constants, and other modules, providing namespacing and mixins for code organization. The module system operates through three primary mechanisms: definition with module
, inclusion via include
, extend
, and prepend
, and nesting for hierarchical organization.
Modules cannot be instantiated directly but integrate with classes through Ruby's method lookup chain. When included, a module's instance methods become available to the including class. Extension adds module methods as singleton methods to the target object. Prepending inserts the module before the class in the method lookup chain, enabling method interception patterns.
module Authenticatable
def authenticate(password)
encrypted_password == encrypt(password)
end
private
def encrypt(value)
# encryption logic
end
end
class User
include Authenticatable
attr_accessor :encrypted_password
end
user = User.new
user.encrypted_password = "abc123"
user.authenticate("plaintext")
# => false (assuming encrypt returns different value)
Module nesting creates hierarchical namespaces that prevent naming conflicts and organize related functionality. Nested modules access outer scope through constant resolution rules, with Ruby searching from innermost to outermost scope.
module Payment
PROCESSING_FEE = 2.5
module CreditCard
def self.process(amount)
total = amount + PROCESSING_FEE # accesses outer constant
"Processing $#{total} via credit card"
end
end
module BankTransfer
PROCESSING_FEE = 1.0 # shadows outer constant
def self.process(amount)
total = amount + PROCESSING_FEE # uses inner constant
"Processing $#{total} via bank transfer"
end
end
end
Ruby provides callback methods (included
, extended
, prepended
) that execute when modules integrate with classes, enabling dynamic behavior modification and hook registration.
Basic Usage
Module definition uses the module
keyword followed by a constant name. Module names follow class naming conventions with CamelCase. Methods defined within modules become instance methods when included or singleton methods when extended.
module Comparable
def >(other)
(self <=> other) > 0
end
def <(other)
(self <=> other) < 0
end
def ==(other)
(self <=> other) == 0
end
end
class Version
include Comparable
attr_reader :major, :minor, :patch
def initialize(version_string)
@major, @minor, @patch = version_string.split('.').map(&:to_i)
end
def <=>(other)
[major, minor, patch] <=> [other.major, other.minor, other.patch]
end
end
v1 = Version.new("2.1.0")
v2 = Version.new("2.0.5")
v1 > v2 # => true
The include
method adds module methods as instance methods to the including class. Multiple modules can be included, with later inclusions appearing first in the method lookup chain. Module methods override class methods when names conflict.
module Debuggable
def debug(message)
puts "[DEBUG] #{self.class.name}: #{message}"
end
end
module Timestamped
def with_timestamp(message)
"#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} - #{message}"
end
end
class APIRequest
include Debuggable
include Timestamped
def process
debug(with_timestamp("Processing request"))
end
end
request = APIRequest.new
request.process
# [DEBUG] APIRequest: 2023-10-15 14:30:25 - Processing request
Module extension with extend
adds module methods as singleton methods to the target object. This pattern works with classes, instances, and other modules. Extended methods become class methods when applied to classes.
module Configurable
def configure(&block)
instance_eval(&block) if block_given?
end
def config
@config ||= {}
end
def set(key, value)
config[key] = value
end
def get(key)
config[key]
end
end
class Database
extend Configurable
end
Database.configure do
set :host, 'localhost'
set :port, 5432
set :database, 'myapp'
end
Database.get(:host) # => "localhost"
Nested modules create namespaces that group related functionality and prevent naming conflicts. Access nested modules using the scope resolution operator ::
. Constants defined in outer modules remain accessible to inner modules unless shadowed by local definitions.
module GraphicsEngine
RENDER_QUALITY = 'high'
module Shapes
class Circle
def render
"Rendering circle with #{RENDER_QUALITY} quality"
end
end
class Rectangle
def render
"Rendering rectangle with #{RENDER_QUALITY} quality"
end
end
end
module Effects
RENDER_QUALITY = 'ultra' # shadows outer constant
class Glow
def render
"Rendering glow effect with #{RENDER_QUALITY} quality"
end
end
end
end
circle = GraphicsEngine::Shapes::Circle.new
circle.render # => "Rendering circle with high quality"
glow = GraphicsEngine::Effects::Glow.new
glow.render # => "Rendering glow effect with ultra quality"
Advanced Usage
Module prepending with prepend
inserts modules before the including class in the method lookup chain, enabling method interception and decoration patterns. Prepended modules can call super
to invoke the original method implementation, creating wrapper behavior around existing functionality.
module Cached
def find(id)
cache_key = "#{self.name}:#{id}"
if cached_value = cache_store[cache_key]
puts "Cache hit for #{cache_key}"
return cached_value
end
result = super # calls original find method
cache_store[cache_key] = result
puts "Cached result for #{cache_key}"
result
end
private
def cache_store
@cache_store ||= {}
end
end
module Logged
def find(id)
start_time = Time.now
puts "Starting find operation for ID: #{id}"
result = super
duration = Time.now - start_time
puts "Find operation completed in #{duration.round(3)}s"
result
end
end
class User
prepend Cached
prepend Logged # this executes first
def self.find(id)
sleep(0.1) # simulate database query
{ id: id, name: "User #{id}", email: "user#{id}@example.com" }
end
end
User.find(1)
# Starting find operation for ID: 1
# Cached result for 1
# Find operation completed in 0.101s
User.find(1)
# Starting find operation for ID: 1
# Cache hit for User:1
# Find operation completed in 0.001s
Module callbacks execute when modules interact with classes, providing hooks for dynamic behavior setup. The included
callback runs when a module is included, receiving the including class as an argument. This enables class method addition and configuration.
module Trackable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
attr_accessor :tracked_attributes
after_initialize :setup_tracking
end
end
module ClassMethods
def track(*attributes)
tracked_attributes.concat(attributes.map(&:to_s))
end
def tracked_attributes
@tracked_attributes ||= []
end
end
def setup_tracking
@changes = {}
@original_values = {}
self.class.tracked_attributes.each do |attr|
@original_values[attr] = send(attr)
define_singleton_method("#{attr}=") do |value|
old_value = send(attr)
@changes[attr] = [old_value, value] if old_value != value
instance_variable_set("@#{attr}", value)
end
end
end
def changes
@changes.dup
end
def changed?
@changes.any?
end
end
class Product
include Trackable
attr_accessor :name, :price, :description
track :name, :price
def initialize(name, price)
@name = name
@price = price
setup_tracking
end
private
def after_initialize(method_name)
# simplified callback simulation
end
end
Module composition creates complex behaviors by combining multiple modules. The order of inclusion affects method resolution, with later modules taking precedence. This enables layered functionality and aspect-oriented programming patterns.
module Validatable
def validate!
errors = []
self.class.validations.each do |field, rules|
value = send(field)
rules.each do |rule, parameter|
case rule
when :presence
errors << "#{field} cannot be blank" if value.nil? || value == ""
when :length
errors << "#{field} too long" if value.to_s.length > parameter
end
end
end
raise StandardError, errors.join(", ") unless errors.empty?
end
def self.included(base)
base.extend(ValidationMethods)
end
module ValidationMethods
def validates(field, **rules)
validations[field] = rules
end
def validations
@validations ||= {}
end
end
end
module Serializable
def to_json
attributes = {}
self.class.serialized_attributes.each do |attr|
attributes[attr] = send(attr)
end
JSON.generate(attributes)
end
def self.included(base)
base.extend(SerializationMethods)
end
module SerializationMethods
def serialize(*attributes)
serialized_attributes.concat(attributes.map(&:to_s))
end
def serialized_attributes
@serialized_attributes ||= []
end
end
end
class Customer
include Validatable
include Serializable
attr_accessor :name, :email, :age
validates :name, presence: true, length: 50
validates :email, presence: true
serialize :name, :email, :age
def initialize(name, email, age)
@name = name
@email = email
@age = age
end
end
Dynamic module creation enables runtime module generation and modification. This pattern supports plugin architectures and configuration-driven behavior where module structure depends on external conditions.
def create_api_client_module(base_url, endpoints)
Module.new do
endpoints.each do |endpoint, config|
define_method(endpoint) do |**params|
url = "#{base_url}#{config[:path]}"
method = config[:method] || :get
case method
when :get
# simulate GET request
"GET #{url} with params: #{params}"
when :post
# simulate POST request
"POST #{url} with data: #{params}"
end
end
end
define_method(:base_url) { base_url }
end
end
api_config = {
users: { path: '/users', method: :get },
create_user: { path: '/users', method: :post },
orders: { path: '/orders', method: :get }
}
APIClient = create_api_client_module('https://api.example.com', api_config)
class ServiceLayer
include APIClient
end
service = ServiceLayer.new
service.users(page: 1, limit: 10)
# => "GET https://api.example.com/users with params: {:page=>1, :limit=>10}"
Common Pitfalls
Constant resolution in nested modules follows Ruby's lexical scoping rules, not inheritance chains. When a constant is referenced within a nested module, Ruby searches the nesting chain from innermost to outermost scope, then checks top-level constants. This behavior creates confusion when constants exist at multiple nesting levels.
TIMEOUT = 30 # top-level constant
module NetworkClient
TIMEOUT = 10
module HTTP
def self.request(url)
# This uses NetworkClient::TIMEOUT, not top-level TIMEOUT
"Making HTTP request with #{TIMEOUT}s timeout" # => 10s timeout
end
def self.fully_qualified_request(url)
# Explicitly reference top-level constant
"Making HTTP request with #{::TIMEOUT}s timeout" # => 30s timeout
end
end
module FTP
TIMEOUT = 60 # shadows outer TIMEOUT
def self.transfer(file)
# This uses FTP::TIMEOUT (60), not NetworkClient::TIMEOUT (10)
"Transferring file with #{TIMEOUT}s timeout" # => 60s timeout
end
end
end
NetworkClient::HTTP.request("example.com")
# => "Making HTTP request with 10s timeout"
NetworkClient::FTP.transfer("file.txt")
# => "Transferring file with 60s timeout"
Method visibility in modules propagates to including classes, but the visibility context depends on where methods are defined. Private and protected methods in modules remain private/protected when included. However, extending a module with private methods makes those methods private singleton methods, often causing unexpected behavior.
module DatabaseHelpers
def connection
@connection ||= establish_connection
end
private
def establish_connection
"Database connection established"
end
def log_query(sql)
puts "Executing: #{sql}"
end
end
class User
include DatabaseHelpers
def find_by_email(email)
log_query("SELECT * FROM users WHERE email = '#{email}'") # Error!
# private method `log_query' called
end
def safe_find_by_email(email)
connection # This works - connection is public
send(:log_query, "SELECT * FROM users WHERE email = '#{email}'")
end
end
# Extending creates different visibility behavior
class Report
extend DatabaseHelpers
end
# Report.establish_connection # Error! private method
# Report.send(:establish_connection) # This works
Multiple inclusion of the same module does not execute inclusion callbacks multiple times, but module nesting and method lookup can create unexpected behavior when modules share names across different nesting levels. Ruby's module inclusion is idempotent - including the same module multiple times has no additional effect.
module Loggable
def self.included(base)
puts "Loggable included in #{base.name}"
end
def log(message)
puts "[LOG] #{message}"
end
end
class Service
include Loggable # "Loggable included in Service"
include Loggable # No output - already included
end
# Name collision example
module Admin
module User
def role
"administrator"
end
end
end
module Guest
module User
def role
"guest"
end
end
end
class Session
include Admin::User
include Guest::User # This overrides Admin::User methods
end
session = Session.new
session.role # => "guest" (not "administrator")
Module variables and state sharing can create memory leaks and unexpected behavior when modules maintain instance variables or class variables. Since modules can be included in multiple classes, shared state becomes problematic across different contexts.
module Cacheable
def self.included(base)
# This creates a class variable shared across ALL including classes
base.class_variable_set(:@@cache, {})
end
def cached_get(key)
cache = self.class.class_variable_get(:@@cache)
cache[key]
end
def cached_set(key, value)
cache = self.class.class_variable_get(:@@cache)
cache[key] = value
end
end
class Product
include Cacheable
end
class Order
include Cacheable
end
# Unexpected sharing - both classes share the same cache!
Product.new.cached_set("key1", "product_value")
Order.new.cached_get("key1") # => "product_value" (unexpected!)
# Better approach - use instance variables per class
module BetterCacheable
def self.included(base)
base.instance_variable_set(:@cache, {})
base.define_singleton_method(:cache) do
@cache
end
end
def cached_get(key)
self.class.cache[key]
end
def cached_set(key, value)
self.class.cache[key] = value
end
end
Circular dependencies between modules can create loading issues and undefined constant errors, particularly in larger applications with complex module hierarchies. Ruby loads constants lazily, so forward references may fail depending on load order.
# In file user.rb
module UserHelpers
def format_name
# This may fail if AddressHelpers not loaded yet
AddressHelpers.format_location(self.location)
end
end
# In file address.rb
module AddressHelpers
def self.format_location(location)
# This creates a circular dependency
UserHelpers.some_method if defined?(UserHelpers)
"Formatted: #{location}"
end
end
# Better approach - avoid circular dependencies
module Formatters
module User
def self.format_name(user, location_formatter = AddressFormatters)
location_formatter.format_location(user.location)
end
end
module Address
def self.format_location(location)
"Formatted: #{location}"
end
end
end
Reference
Core Module Methods
Method | Parameters | Returns | Description |
---|---|---|---|
module Name |
Name (Constant) |
Module |
Defines a new module with given name |
#include(module) |
module (Module) |
self |
Includes module as instance methods |
#extend(module) |
module (Module) |
self |
Extends object with module methods as singleton methods |
#prepend(module) |
module (Module) |
self |
Prepends module before class in method lookup |
#included_modules |
None | Array |
Returns array of included modules |
#ancestors |
None | Array |
Returns ancestor chain including modules |
Module Callback Methods
Callback | When Called | Parameters | Description |
---|---|---|---|
included(base) |
Module included in class/module | base (Class/Module) |
Hook for include operations |
extended(object) |
Module extends object | object (Object) |
Hook for extend operations |
prepended(base) |
Module prepended to class | base (Class/Module) |
Hook for prepend operations |
Constant Resolution Rules
Context | Resolution Order | Example |
---|---|---|
Nested Module | Inner → Outer → Top-level | A::B::C searches C , then B , then A , then top-level |
Class Method | Class → Superclasses → Modules → Top-level | Instance method constant lookup |
Top-level | Current scope → Top-level | Global constant access |
Module Inclusion Effects
Operation | Method Location | Lookup Chain Position | Visibility |
---|---|---|---|
include |
Instance methods | After including class | Inherits module visibility |
extend |
Singleton methods | Object's singleton class | Inherits module visibility |
prepend |
Instance methods | Before including class | Inherits module visibility |
Visibility Modifiers in Modules
Modifier | Scope | Effect on Including Class | Example |
---|---|---|---|
public |
Default | Methods become public | Accessible from anywhere |
private |
Module only | Methods become private | Only callable with send |
protected |
Same object/class | Methods become protected | Accessible within class hierarchy |
Common Module Patterns
Pattern | Implementation | Use Case |
---|---|---|
Mixin | include ModuleName |
Add instance functionality |
Class Extension | extend ModuleName |
Add class methods |
Namespace | Module::Class |
Organize related classes |
Callback Hook | def self.included(base) |
Dynamic class modification |
Method Interception | prepend with super |
Wrap existing methods |
Error Types
Error | Cause | Resolution |
---|---|---|
NameError: uninitialized constant |
Constant not found in scope | Check constant definition and scope |
NoMethodError: private method called |
Calling private module method | Use send or make method public |
ArgumentError: cyclic include detected |
Module includes itself | Remove circular inclusion |
TypeError: wrong argument type |
Invalid module argument | Pass module object to include/extend |
Module Inspection Methods
Method | Returns | Description |
---|---|---|
#name |
String |
Module name or nil for anonymous |
#const_defined?(name) |
Boolean |
Whether constant exists in module |
#const_get(name) |
Object |
Value of named constant |
#const_set(name, value) |
Object |
Sets constant value |
#constants |
Array |
Array of constant names |
#method_defined?(name) |
Boolean |
Whether instance method exists |
#private_method_defined?(name) |
Boolean |
Whether private method exists |