Ancestors chain: how Ruby actually resolves methods
The favorite interview question to separate a Rubyist from someone who just writes Ruby. Include, prepend, singleton classes — every doubt has the same answer.
Ancestors chain: how Ruby actually resolves methods
The favorite interview question to separate a Rubyist from "someone who writes Ruby":
"Explain the ancestors chain off the top of your head."
If you stuttered, no problem.
This article is so you never stutter again.
What the ancestors chain is
The ancestors chain is the ordered list of classes and modules Ruby walks through to resolve a method.
You can see it:
String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]
When you call "abc".upcase, Ruby looks for upcase:
String→ found. Use it.
When you call "abc".tap { }:
String→ not thereComparable→ not thereObject→ not thereKernel→ found. Use it.
The chain is literally walked.
The basic include rule
module M
def greet = "hi from M"
end
class A
include M
end
A.ancestors
# => [A, M, Object, Kernel, BasicObject]
include adds the module above the class that included it, below the next ancestor.
Order matters. Always.
prepend changes the game
module M
def greet = "from M, then #{super}"
end
class A
prepend M
def greet = "from A"
end
A.ancestors
# => [M, A, Object, Kernel, BasicObject]
A.new.greet
# => "from M, then from A"
prepend slots the module below the class — before it in the chain.
That means: the module intercepts the call before the class itself.
super there calls the class's version.
It's how Active Support patches methods without rewriting the class.
Multiple include — reverse order
module A; end
module B; end
class C
include A
include B
end
C.ancestors
# => [C, B, A, Object, Kernel, BasicObject]
The last one included sits closest to the class.
Makes sense if you think about it: each include pushes whatever came before backward.
Inheritance + module
module M
def hello = "M"
end
class Parent
def hello = "Parent"
end
class Child < Parent
include M
end
Child.ancestors
# => [Child, M, Parent, Object, Kernel, BasicObject]
Child.new.hello # => "M"
M sits between Child and Parent.
Child didn't define hello → Ruby looks at M → found → uses it.
If M called super, it would go to Parent#hello.
Singleton classes complicate things
Every object has its invisible singleton class.
user = User.new
def user.special = "just me"
Where does special live?
In user's singleton class, which is an extra layer before User.
user
↓
[singleton of user]
↓
User
↓
...
Class methods (def self.foo) are singleton methods of the class itself.
That's why class << self is the syntax for opening that box.
Multiple prepends — reverse order
Same as include, but the other way:
module A; end
module B; end
class C
prepend A
prepend B
end
C.ancestors
# => [B, A, C, Object, Kernel, BasicObject]
B is the "outermost" — intercepts first.
Think onion. Each prepend adds a layer on the outside.
Real scenario: Devise
When you add devise :database_authenticatable to User, it does an include of a monster module.
That module has valid_password?, valid_for_authentication?, etc.
Then you customize:
class User
devise :database_authenticatable
def valid_password?(password)
# my custom logic
super
end
end
Your valid_password? is on the class.
Devise's is on the module.
Ancestors:
[User, DeviseModules..., ApplicationRecord, ...]
When you call super, it goes to DeviseModules.
Without understanding the chain, this is magic.
With it, it's mechanical.
When the chain bites you
- monkey patch that silently overrode something
includeordered wrong, making the wrong method winprependat runtime invalidating the VM's inline cache- a gem that auto-
includes into ActiveRecord::Base and breaks something you didn't know existed
Every time something "strange" happens with a method in Ruby, the answer is in SuspectClass.ancestors.
The big shift
The ancestors chain isn't trivia.
It's Ruby's dynamic type system in action.
When you understand the chain, you understand:
- how Rails is composed
- how Devise/Pundit/etc graft into your model
- why some refactors break everything
- how to write DSLs without destroying the hierarchy
Conclusion
Every doubt about "why is this method being called?" has the same answer:
Look at the ancestors chain.
You can debug Ruby your whole life without it.
But you'll suffer more than you should.
And whoever knows the chain off the top of their head is the one who stops calling Ruby magical — and starts calling it well-designed.
