githublinkedinemail
← /articles

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.

4 minruby

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:

  1. String → found. Use it.

When you call "abc".tap { }:

  1. String → not there
  2. Comparable → not there
  3. Object → not there
  4. Kernel → 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
  • include ordered wrong, making the wrong method win
  • prepend at 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.


Ancestors chain: how Ruby actually resolves methods
Ancestors chain: how Ruby actually resolves methods