
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.
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" endendmodule Dry def process puts "Dry" endendclass Plate include Wash include DryendPlate.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.

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:
- The class itself
- Included modules
- Parent classes
- Modules included in parent classes
- Object
- Kernel
- 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 endendclass Plate prepend Logging def process puts "Processing" endendPlate.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.
