/ Tags: RUBY 3 / Categories: RUBY

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, :foo followed by a new def foo that calls original_foo — that’s almost always a sign that prepend is the cleaner solution. Prepend keeps the method chain intact, shows up clearly in ancestors, 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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More