← Back to blog

Understanding Ruby's Method Lookup Path

· Lachlan Young

You call a method on an object. Ruby finds it and runs it. Simple, right? Except sometimes Ruby finds the wrong one. Or it finds nothing and you get a NoMethodError on something you’re sure should work. Or a module you included seems to be ignored entirely.

When these bugs show up, and they will, understanding Ruby’s method lookup path is the difference between debugging for five minutes and debugging for an hour.

The basic chain

When you call object.some_method, Ruby follows a specific, deterministic path to find the method definition. Here’s the order:

  1. The object’s singleton class (methods defined on that specific object)
  2. The object’s class
  3. Modules included in that class (last included wins)
  4. The superclass
  5. Modules included in the superclass
  6. Up the chain until BasicObject
  7. If still not found, start method_missing from the bottom again

This isn’t abstract theory. You can see the exact chain for any object:

class Animal
  def speak
    "..."
  end
end

module Trainable
  def sit
    "sitting"
  end
end

module Domesticated
  def loyal?
    true
  end
end

class Dog < Animal
  include Trainable
  include Domesticated

  def fetch
    "fetching"
  end
end

Dog.ancestors
# => [Dog, Domesticated, Trainable, Animal, Object, Kernel, BasicObject]

That array is the method lookup path. When you call a method on a Dog instance, Ruby walks that list from left to right, checking each entry for a matching method definition. First match wins.

Last included wins

Notice the order of the modules: Domesticated comes before Trainable in the ancestors chain, even though Trainable was included first. This is because Ruby inserts included modules directly above the including class, and later includes go higher in the stack.

This matters when two modules define the same method:

module A
  def greet
    "hello from A"
  end
end

module B
  def greet
    "hello from B"
  end
end

class MyClass
  include A
  include B
end

MyClass.new.greet  # => "hello from B"
MyClass.ancestors  # => [MyClass, B, A, Object, Kernel, BasicObject]

B was included last, so it appears first in the ancestor chain after MyClass. Its greet gets found first. If you expected A’s version, you’d be confused, until you check .ancestors.

Prepend: the one that goes in front

include inserts a module after the class in the lookup chain. prepend inserts it before.

module Logging
  def save
    puts "about to save..."
    super
    puts "saved!"
  end
end

class Record
  prepend Logging

  def save
    puts "saving record"
  end
end

Record.ancestors
# => [Logging, Record, Object, Kernel, BasicObject]

Record.new.save
# about to save...
# saving record
# saved!

Logging comes before Record in the chain. So when you call save, Ruby finds Logging#save first. The super call inside it then continues up the chain to Record#save.

This is incredibly useful for wrapping behavior around existing methods without modifying the original class. It’s how many gems add instrumentation, caching, or validation around your methods. And it’s why prepend largely replaced the old alias_method_chain hack.

Singleton methods: the per-object override

Ruby lets you define methods on individual objects, not just classes:

dog = Dog.new

def dog.tricks
  "roll over"
end

Where does this method live? In the object’s singleton class (also called its eigenclass or metaclass). The singleton class sits at the very front of the lookup chain, before even the object’s actual class.

dog.singleton_class.ancestors
# => [#<Class:#<Dog:0x...>>, Dog, Domesticated, Trainable, Animal, Object, Kernel, BasicObject]

That #<Class:#<Dog:0x...>> at the front is the singleton class. Any method defined there takes priority over everything else. This is also how class methods work, they’re singleton methods on the class object itself.

Where method_missing fits

If Ruby walks the entire ancestor chain and finds nothing, it doesn’t give up immediately. It starts over from the bottom, this time looking for method_missing. If any class or module in the chain defines method_missing, that gets called with the method name and arguments.

class FlexibleConfig
  def method_missing(name, *args)
    if name.to_s.end_with?("=")
      instance_variable_set("@#{name.to_s.chomp('=')}", args.first)
    else
      instance_variable_get("@#{name}")
    end
  end

  def respond_to_missing?(name, include_private = false)
    true
  end
end

config = FlexibleConfig.new
config.database = "postgres"
config.database  # => "postgres"

This is powerful but dangerous. If you define method_missing without also defining respond_to_missing?, introspection breaks. And if you forget to call super for methods you don’t handle, you’ll swallow errors silently.

The rule of thumb: avoid method_missing unless you’re building a DSL or proxy object, and always define respond_to_missing? alongside it.

Debugging with ancestors

When you hit a “wrong method” bug, .ancestors is your best friend. Here’s the workflow:

# What class is this object?
object.class
# => SomeClass

# What's the full lookup chain?
object.class.ancestors
# => [SomeClass, ModuleB, ModuleA, ParentClass, Object, Kernel, BasicObject]

# Where is this specific method defined?
object.method(:problematic_method).owner
# => ModuleA

# What's the source location?
object.method(:problematic_method).source_location
# => ["/path/to/file.rb", 42]

That last one, source_location, is gold. It tells you exactly which file and line number defines the method Ruby is actually calling. When a method behaves unexpectedly, this is the fastest way to find out if you’re even looking at the right definition.

The practical takeaway

You don’t need to think about method lookup on every method call. But when something goes wrong, when a method returns unexpected results, when an override doesn’t seem to take effect, when super calls the wrong parent, this is the mental model that gets you unstuck.

The rules are simple and consistent: singleton class first, then class, then modules (last included first), then superclass, repeat. When in doubt, print .ancestors and read it left to right. The answer is always in that list.


Comparing complex Ruby objects and the diff is hard to read? Try RubyHash, paste your expected and actual output for a clean, highlighted comparison.

Enjoyed this post?

Subscribe to get notified when we publish more Ruby and Rails content.