Skip to main content
โšก Calmops

Ruby prepend vs include: Module Composition Guide

Introduction

Ruby 2.0 introduced prepend as a new way to mix modules into classes. While include adds a module’s methods after the class in the method lookup chain, prepend inserts them before the class โ€” giving you the ability to wrap or override existing methods without monkey-patching.

Understanding the difference between prepend and include is key to writing clean, composable Ruby code.

The Method Lookup Chain

Ruby resolves method calls by walking an ordered list called the method lookup chain (or ancestor chain). You can inspect it with .ancestors:

class Animal; end
puts Animal.ancestors.inspect
# => [Animal, Object, Kernel, BasicObject]

When you include or prepend a module, it gets inserted into this chain at different positions.

include: Module Goes After the Class

With include, the module is inserted after the class in the ancestor chain:

module Greetable
  def hello
    "Hello from Greetable"
  end
end

class Person
  include Greetable

  def hello
    "Hello from Person"
  end
end

puts Person.ancestors.inspect
# => [Person, Greetable, Object, Kernel, BasicObject]

puts Person.new.hello
# => "Hello from Person"  (Person's method wins)

Because Person comes before Greetable in the chain, Person#hello takes precedence.

prepend: Module Goes Before the Class

With prepend, the module is inserted before the class:

module Greetable
  def hello
    "Hello from Greetable"
  end
end

class Person
  prepend Greetable

  def hello
    "Hello from Person"
  end
end

puts Person.ancestors.inspect
# => [Greetable, Person, Object, Kernel, BasicObject]

puts Person.new.hello
# => "Hello from Greetable"  (Greetable's method wins)

Now Greetable#hello is called first. This is the key difference.

The Power of prepend: Wrapping Methods

The most useful pattern with prepend is wrapping an existing method โ€” calling super to invoke the original while adding behavior around it:

module Logging
  def save
    puts "[LOG] Before save"
    result = super  # calls the original Person#save
    puts "[LOG] After save"
    result
  end
end

class Person
  prepend Logging

  def save
    puts "Saving person..."
    true
  end
end

Person.new.save
# => [LOG] Before save
# => Saving person...
# => [LOG] After save

This is a clean alternative to alias-based monkey-patching.

Practical Use Cases

1. Adding Instrumentation / Metrics

module Instrumented
  def process(data)
    start = Time.now
    result = super
    elapsed = Time.now - start
    puts "process() took #{elapsed.round(4)}s"
    result
  end
end

class DataPipeline
  prepend Instrumented

  def process(data)
    # heavy processing...
    data.upcase
  end
end

DataPipeline.new.process("hello")
# => process() took 0.0001s

2. Input Validation

module Validated
  def create_user(name, age)
    raise ArgumentError, "Name can't be blank" if name.to_s.strip.empty?
    raise ArgumentError, "Age must be positive" unless age.to_i > 0
    super
  end
end

class UserService
  prepend Validated

  def create_user(name, age)
    puts "Creating user: #{name}, age #{age}"
  end
end

UserService.new.create_user("Alice", 30)  # => Creating user: Alice, age 30
UserService.new.create_user("", 30)       # => ArgumentError: Name can't be blank

3. Caching

module Memoizable
  def expensive_calculation(n)
    @cache ||= {}
    @cache[n] ||= super
  end
end

class MathService
  prepend Memoizable

  def expensive_calculation(n)
    sleep(0.1)  # simulate heavy work
    n * n
  end
end

svc = MathService.new
puts svc.expensive_calculation(5)  # slow first time
puts svc.expensive_calculation(5)  # instant from cache

include vs prepend: Side-by-Side Comparison

module M
  def who
    "M -> #{super rescue 'end'}"
  end
end

class WithInclude
  include M
  def who; "WithInclude"; end
end

class WithPrepend
  prepend M
  def who; "WithPrepend"; end
end

puts WithInclude.new.who   # => "WithInclude"  (class wins)
puts WithPrepend.new.who   # => "M -> WithPrepend"  (module wraps class)

puts WithInclude.ancestors.first(3).inspect
# => [WithInclude, M, Object]

puts WithPrepend.ancestors.first(3).inspect
# => [M, WithPrepend, Object]

When to Use Each

Scenario Use
Adding new methods to a class include
Sharing utility methods across classes include
Wrapping / decorating existing methods prepend
Adding before/after hooks without alias prepend
Instrumentation, logging, caching layers prepend

Multiple prepend Calls

You can prepend multiple modules. They stack in reverse order of declaration:

module A
  def greet; "A -> #{super}"; end
end

module B
  def greet; "B -> #{super}"; end
end

class Base
  prepend A, B

  def greet; "Base"; end
end

puts Base.ancestors.first(4).inspect
# => [A, B, Base, Object]

puts Base.new.greet
# => "A -> B -> Base"

Common Pitfalls

Forgetting super

If you prepend a module to wrap a method but forget super, the original method never runs:

module BadWrapper
  def save
    puts "Before save"
    # forgot super โ€” original save never called!
  end
end

Prepending to the Wrong Level

prepend on a module affects instances. To affect class-level methods, use prepend inside class << self:

module ClassLogging
  def find(id)
    puts "Finding #{id}..."
    super
  end
end

class User
  class << self
    prepend ClassLogging
  end

  def self.find(id)
    "User ##{id}"
  end
end

puts User.find(42)
# => Finding 42...
# => User #42

Resources

Comments