include, extend, and prepend — Ruby's Three Ways to Use Modules
Ruby modules are one of the language’s most flexible tools — they handle namespacing, mixins, and code sharing without the rigidity of inheritance. But include, extend, and prepend are three different operations that place module methods in very different parts of the lookup chain. Most developers learn include early and assume the others are edge cases. They’re not — each solves a specific problem, and confusing them leads to subtle bugs that are annoying to track down.
The Method Lookup Chain
Before getting into the three methods, the foundation: when you call a method on an object, Ruby searches for it in a specific order — the object’s singleton class, its class, then ancestors up the chain (modules included in the class, then superclasses and their modules). This chain is what include, extend, and prepend manipulate.
Example:
class User; end
User.ancestors # => [User, Object, Kernel, BasicObject]
Each of the three module methods inserts the module into this chain at a different position.
include — Instance Methods from a Module
include is the most common. It adds module methods to the class’s instance method chain, inserted after the class itself:
Example:
module Greetable
def greet
"Hello, I'm #{name}"
end
end
class User
include Greetable
attr_reader :name
def initialize(name)
@name = name
end
end
User.ancestors
# => [User, Greetable, Object, Kernel, BasicObject]
User.new("Alice").greet # => "Hello, I'm Alice"
User.new("Alice").is_a?(Greetable) # => true
The module sits after User in the ancestor chain. If User defines a method with the same name as the module, User’s version wins (it’s higher in the chain).
Use include for: sharing instance methods across multiple classes — authentication concerns, serialization, audit logging, domain behavior.
extend — Class Methods from a Module
extend adds module methods as class-level methods (on the class object itself, not instances):
Example:
module Searchable
def search_by_name(query)
where("name ILIKE ?", "%#{query}%")
end
def active
where(active: true)
end
end
class User < ApplicationRecord
extend Searchable
end
User.search_by_name("alice") # class method
User.active # class method
extend on a class is equivalent to include-ing into the class’s singleton class. You can also extend individual objects (not just classes) to add methods to a single instance:
Example:
module Exportable
def export_csv
to_csv
end
end
report = Report.new
report.extend(Exportable)
report.export_csv # only this instance has the method
other_report = Report.new
other_report.export_csv # => NoMethodError — not extended
Use extend for: class-level query scopes, factory methods, configuration DSLs, single-object behavior augmentation.
include + extend together
A common pattern — add both instance and class methods from one module using a callback:
Example:
module Auditable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def audited_attributes
@audited_attributes ||= []
end
def audit(*attrs)
audited_attributes.concat(attrs)
end
end
def changes_for_audit
audited_attributes = self.class.audited_attributes
previous_changes.slice(*audited_attributes)
end
end
class Order < ApplicationRecord
include Auditable
audit :status, :total_price
end
Order.audited_attributes # => [:status, :total_price] (class method)
order.changes_for_audit # instance method
ActiveSupport::Concern wraps this pattern cleanly — it’s worth understanding the underlying mechanism before reaching for the convenience wrapper.
prepend — Override Class Methods Without Monkey-Patching
prepend inserts the module before the class in the ancestor chain — meaning the module’s methods are searched before the class’s own methods:
Example:
module Timestamped
def save
self.updated_at = Time.current
super
end
end
class Record
prepend Timestamped
attr_accessor :name, :updated_at
def save
puts "Saving #{name}"
end
end
Record.ancestors
# => [Timestamped, Record, Object, Kernel, BasicObject]
r = Record.new
r.name = "Test"
r.save
# Sets updated_at, then calls Record#save via super
# => "Saving Test"
prepend is the clean way to wrap an existing method. Without it, you’d need to alias the original method, override it, then call the alias — classic monkey-patching that’s brittle and hard to debug. With prepend, the module sits in front, calls super, and the original method runs normally.
Use prepend for: transparently wrapping existing methods with cross-cutting behavior (caching, logging, instrumentation, validation), without modifying the original class.
Pro-Tip: When you see alias-based monkey-patching in a codebase —
alias_method :original_foo, :foofollowed by a newdef foothat callsoriginal_foo— that’s almost always a sign thatprependis the cleaner solution. Prepend keeps the method chain intact, shows up clearly inancestors, and doesn’t pollute the class with aliased method names.
Quick Reference
| Method | Where methods land | Used for |
|---|---|---|
include |
After the class in ancestors | Instance method mixins |
extend |
On the singleton (class) object | Class-level methods |
prepend |
Before the class in ancestors | Wrapping/overriding existing methods |
Conclusion
include, extend, and prepend aren’t interchangeable — each puts module methods in a specific place in the lookup chain, and that placement determines behavior. Most Ruby developers are comfortable with include; the other two are worth deliberate practice. Once you’ve used prepend to cleanly wrap a method that previously required alias gymnastics, or used extend to share class-level scopes across models, you’ll reach for them naturally. Ruby’s module system is one of the language’s most elegant designs — understanding all three entry points makes it fully usable.
FAQs
Q1: Can I include, extend, and prepend the same module?
Technically yes, but it’s confusing. Each operation serves a distinct purpose — if you need both instance and class methods from a module, use the included callback pattern or ActiveSupport::Concern to handle both cleanly in one include call.
Q2: What’s ActiveSupport::Concern and how does it relate to this?
Concern is a Rails module that wraps the included callback pattern. It lets you define both module ClassMethods (for class-level methods via extend) and instance-level methods in one clean DSL. Under the hood it uses include and extend — understanding those makes Concern’s behavior predictable.
Q3: Does prepend affect performance?
Slightly — adding a level to the ancestor chain means one more lookup step. In practice this is negligible. Profile only if you’re calling a prepended method millions of times in a tight loop.
Q4: Can modules be prepended to other modules?
Yes. Module#prepend works on modules too, not just classes. The module receiving the prepend gets the prepended module inserted before it in its ancestor chain. This is uncommon but valid.
Q5: How do I see the full ancestor chain of a class?
Call ClassName.ancestors — it returns an array showing the complete method lookup order. This is the fastest way to debug unexpected behavior when multiple modules are involved.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀