Ruby’s Ancestor Chain: Why prepend Cuts the Line

June 7, 2026

When Ruby receives a method call, it follows a well-defined search path to determine where that method is implemented. Most developers learn inheritance early, but fewer take the time to understand the complete method lookup path, also known as the ancestor chain.

Understanding this mechanism can make debugging easier, clarify how Rails works under the hood, and explain why prepend behaves differently from include.

Tokyo Topographic Map
Built for Ruby on Rails

Build Maps Without
Google APIs

Generate beautiful production-ready maps directly from your Rails backend. Fast rendering, zero external dependencies, full control.

✓ No API fees ✓ Self-hosted ✓ Rails Native ✓ Fast Rendering
Why developers switch
Replace expensive map stacks.

Stop relying on third-party map billing and bloated JS libraries. Render static or dynamic maps directly in Ruby.

Try It Now
Tokyo MapView Demo

A Stack of Dishes

Imagine a pile of dishes waiting to be washed.

The last dish placed on the pile is the first one to be taken off. This is the classic LIFO (Last In, First Out) principle.

Ruby’s ancestor chain behaves similarly.

When modules are included in a class, Ruby places them into the lookup path. The most recently included module gets higher priority than modules included earlier.

Consider the following example:

module Wash
def process
puts "Wash"
end
end
module Dry
def process
puts "Dry"
end
end
class Plate
include Wash
include Dry
end
Plate.ancestors

Output:

[Plate, Dry, Wash, Object, Kernel, BasicObject]

Notice that Dry appears before Wash.

Although Wash was included first, Dry was included later and receives priority during method lookup.

If we execute:

Plate.new.process

Ruby will find process in Dry before reaching Wash.

The last module added gets searched first, much like the last dish added to a stack.

Article content

Following the Lookup Path

The ancestors method exposes Ruby’s method lookup order.

Plate.ancestors

Output:

[Plate, Dry, Wash, Object, Kernel, BasicObject]

When a method is called, Ruby walks this list from left to right until it finds a matching implementation.

The search order is:

  1. The class itself
  2. Included modules
  3. Parent classes
  4. Modules included in parent classes
  5. Object
  6. Kernel
  7. BasicObject

This chain is the foundation of Ruby’s object model.

Then prepend Appears

Now let’s introduce prepend.

module Logging
def process
puts "Before process"
super
end
end
class Plate
prepend Logging
def process
puts "Processing"
end
end
Plate.ancestors

Output:

[Logging, Plate, Object, Kernel, BasicObject]

This result surprises many developers.

Unlike include, which inserts a module after the class in the lookup chain, prepend inserts the module before the class itself.

Using our dish analogy, prepend doesn’t follow the stack’s rules.

It walks into the kitchen, jumps ahead of every dish already waiting, and places itself at the very top.

When Ruby searches for process, it finds Logging#process first.

Only after super is called does Ruby continue searching the ancestor chain and eventually reach Plate#process.

Why Rails Developers Should Care

If you’ve worked with Rails, you’ve probably used functionality built on top of the ancestor chain without realizing it.

Examples include:

  • ActiveSupport concerns
  • Authentication libraries
  • Instrumentation wrappers
  • Logging extensions
  • Monkey patches
  • Framework internals

Many of these tools rely on modules being inserted into the lookup path to intercept or extend behavior.

Understanding ancestors, include, and prepend makes these patterns far less mysterious.

Inspecting the Chain

Whenever you’re unsure where a method is coming from, ask Ruby directly:

SomeClass.ancestors

This simple command often reveals why a method behaves differently than expected.

It can expose included modules, prepended modules, framework extensions, and monkey patches that are affecting the final behavior.

Final Thoughts

Inheritance is only part of Ruby’s method lookup story.

The real picture is the ancestor chain.

Included modules join the line and participate in the lookup process according to their insertion order. Prepended modules, however, are given special treatment and move to the front of the chain.

If include follows the stack’s rules, prepend is the dish that cuts the line.

And once you understand that, many of Ruby’s most elegant metaprogramming techniques become much easier to understand.

Article content

Leave a comment