Ruby's object model forms the foundation of how the language organizes classes, modules, and method resolution. Understanding this system reveals why Ruby behaves the way it does and enables you to write more effective object-oriented code. At its core, Ruby follows a simple principle: everything is an object, and every object has a class.
The method lookup chain determines how Ruby finds and executes methods when you call them on objects. This process involves traversing a specific path through classes, modules, and inheritance hierarchies. When you call a method, Ruby doesn't just look in the obvious place - it follows a well-defined sequence that includes the object's singleton methods, included modules, and ancestor classes.
This systematic approach to method resolution enables Ruby's flexible features like mixins, method overriding, and dynamic method definition. The lookup process happens transparently, but understanding it helps you predict behavior, debug issues, and leverage Ruby's metaprogramming capabilities effectively.
In this article, we’ll explore Ruby’s class hierarchy and how it affects method lookup, and the process of resolving method calls.
Let's dive in!
Prerequisites
You'll need Ruby 2.7 or later installed. The object model examples work consistently across Ruby versions, though newer versions provide better introspection tools:
ruby --version
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [arm64-darwin24]
Basic familiarity with Ruby classes and modules will help you follow the examples, though we'll build concepts from the ground up:
ruby -e "puts 'Ready to explore Ruby\'s object model!'"
Ready to explore Ruby's object model!
Setting up the environment
To effectively demonstrate Ruby's object model, you'll create a practical example that reveals how method lookup works in real scenarios. This exploration will show you exactly how Ruby resolves method calls and how different language features interact within the object model.
The key insight we'll uncover is that Ruby's apparent simplicity masks a sophisticated system for organizing behavior and data. By starting with concrete examples and then examining Ruby's internal representations, you'll develop an intuitive understanding of how the object model operates.
Create your project directory:
mkdir ruby-object-model-demo && cd ruby-object-model-demo
Create a foundation that demonstrates basic object relationships:
class User
def authenticate(password)
"Checking credentials..."
end
end
class AdminUser < User
def authenticate(password)
"Admin authentication: #{super}"
end
def manage_users
"Managing user accounts"
end
end
admin = AdminUser.new
puts "AdminUser.superclass: #{AdminUser.superclass}"
puts "admin.authenticate('secret'): #{admin.authenticate('secret')}"
puts "admin.manage_users: #{admin.manage_users}"
This foundation establishes a simple inheritance hierarchy that demonstrates fundamental object model concepts. The AdminUser
class inherits behavior from User
while defining specialized methods for administrative functionality.
When you call admin.authenticate
, Ruby finds the method in AdminUser
, which uses super
to call the parent method. When you call admin.manage_users
, Ruby finds it directly in AdminUser
. This automatic traversal forms the basis of Ruby's method lookup system.
Run this to see the basic relationships:
ruby object_basics.rb
AdminUser.superclass: User
admin.authenticate('secret'): Admin authentication: Checking credentials...
admin.manage_users: Managing user accounts
Notice how admin.authenticate
combines both the AdminUser
and User
versions through super
, while admin.manage_users
calls the method defined only in AdminUser
. This demonstrates Ruby's method lookup process in action.
Understanding the method lookup chain
Now that you've seen basic inheritance in action, you'll explore Ruby's complete method lookup process. This system determines exactly how Ruby finds methods when you call them, involving more than just simple inheritance. The lookup chain includes singleton methods, included modules, and the full inheritance hierarchy, creating a sophisticated resolution system.
Ruby's method lookup follows a specific order that prioritizes more specific definitions over general ones. This means singleton methods (methods defined on individual objects) take precedence over class methods, which take precedence over inherited methods. Understanding this order helps you predict method behavior and design effective class hierarchies.
The lookup process happens automatically and efficiently, but you can examine it using Ruby's introspection capabilities. The ancestors
method reveals the exact order Ruby follows when searching for methods, while other tools let you examine where specific methods are defined.
Extend your basic example to demonstrate the complete lookup process:
# Previous code...
# Examine method lookup chain
puts "AdminUser.ancestors: #{AdminUser.ancestors}"
# Complex lookup with modules
class ApiClient
def request(endpoint)
"GET #{endpoint}"
end
end
module Cacheable
def request(endpoint)
"Cached: #{super}"
end
end
class CachedApiClient < ApiClient
include Cacheable
end
client = CachedApiClient.new
puts "CachedApiClient.ancestors: #{CachedApiClient.ancestors}"
puts "client.request('/users'): #{client.request('/users')}"
The ancestors
method reveals the exact order Ruby follows during method lookup. When you include the Cacheable
module, Ruby inserts it between CachedApiClient
and ApiClient
, allowing the module to enhance inherited behavior using super
.
Run the enhanced version to see the complete lookup chains:
ruby object_basics.rb
AdminUser.superclass: User
admin.authenticate('secret'): Admin authentication: Checking credentials...
admin.manage_users: Managing user accounts
AdminUser.ancestors: [AdminUser, User, Object, Kernel, BasicObject]
CachedApiClient.ancestors: [CachedApiClient, Cacheable, ApiClient, Object, Kernel, BasicObject]
client.request('/users'): Cached: GET /users
The AdminUser.ancestors
result reveals that Ruby automatically includes Kernel
in every class's lookup chain, providing core methods like puts
and require
.
The CachedApiClient.ancestors
chain shows how Cacheable
appears between CachedApiClient
and ApiClient
. When client.request
executes, Ruby finds the method in Cacheable
first, which uses super
to call the original method in ApiClient
.
Exploring singleton methods and eigenclasses
While class-level method definitions affect all instances of a class, Ruby also allows you to define methods on individual objects. These singleton methods exist only on the specific object where they're defined, creating unique behavior without affecting other instances of the same class. This capability enables Ruby's flexible metaprogramming features and explains how class methods actually work.
Behind the scenes, Ruby implements singleton methods using eigenclasses (also called singleton classes). Each object can have an eigenclass that sits at the very beginning of its method lookup chain, before the object's actual class. When you define a singleton method, Ruby creates this eigenclass if it doesn't exist and stores the method there.
This mechanism explains many Ruby features that might seem mysterious at first. Class methods are actually singleton methods defined on class objects. The self
keyword in class definitions refers to the class object itself, and defining methods with def self.method_name
creates singleton methods on that class object.
Create a new file to explore singleton methods and eigenclasses:
class User
def initialize(email)
@email = email
end
def profile
"User: #{@email}"
end
end
alice = User.new("alice@example.com")
bob = User.new("bob@example.com")
# Define singleton methods on alice only
def alice.admin_access
"Full admin privileges"
end
alice.define_singleton_method(:debug_info) do
"Debug mode enabled for #{@email}"
end
puts "alice.profile: #{alice.profile}"
puts "alice.admin_access: #{alice.admin_access}"
puts "alice.debug_info: #{alice.debug_info}"
puts "alice.singleton_methods: #{alice.singleton_methods}"
puts "bob.singleton_methods: #{bob.singleton_methods}"
This example shows how singleton methods create unique behavior on individual objects. Both alice
and bob
are User
instances, but only alice
has the specialized methods stored in her eigenclass.
Run this to see singleton methods in action:
ruby singleton_methods.rb
alice.profile: User: alice@example.com
alice.admin_access: Full admin privileges
alice.debug_info: Debug mode enabled for alice@example.com
alice.singleton_methods: [:debug_info, :admin_access]
bob.singleton_methods: []
Now extend the example to show how class methods work through eigenclasses:
# Previous code...
# Class methods as singleton methods
class DatabaseConnection
def connect
"Connected to database"
end
def self.pool_size
"Current pool: 10 connections"
end
class << self
def health_check
"Database status: OK"
end
end
end
puts "DatabaseConnection.pool_size: #{DatabaseConnection.pool_size}"
puts "DatabaseConnection.health_check: #{DatabaseConnection.health_check}"
puts "DatabaseConnection.singleton_methods: #{DatabaseConnection.singleton_methods}"
Class methods are singleton methods defined on the class object itself. The different syntaxes achieve the same result - creating methods that exist on the class rather than instances.
Run the complete example:
ruby singleton_methods.rb
alice.profile: User: alice@example.com
alice.admin_access: Full admin privileges
alice.debug_info: Debug mode enabled for alice@example.com
alice.singleton_methods: [:debug_info, :admin_access]
bob.singleton_methods: []
DatabaseConnection.pool_size: Current pool: 10 connections
DatabaseConnection.health_check: Database status: OK
DatabaseConnection.singleton_methods: [:health_check, :pool_size]
The DatabaseConnection.singleton_methods
output confirms that class methods are singleton methods on the class object. Individual instances don't have these methods - they exist only on the class object itself.
Module inclusion and method lookup precedence
Modules provide Ruby's mechanism for sharing code between classes without using inheritance. When you include a module in a class, Ruby inserts it into the method lookup chain, creating a form of multiple inheritance. Understanding how modules integrate into lookup chains helps you design effective mixin strategies and predict method resolution behavior.
The position where modules appear in the lookup chain depends on how they're incorporated. The include
keyword places modules between the class and its superclass, while prepend
places them before the class itself in the lookup chain. This positioning difference has important implications for method overriding and super
calls.
Multiple modules included in the same class create a stack-like structure where the most recently included module appears first in the lookup chain. This ordering affects which version of a method gets called when multiple modules define methods with the same name.
Create a new file to explore module inclusion patterns:
# Practical modules with overlapping methods
module Loggable
def process
"Processing..."
end
end
module Timestamped
def process
"#{Time.now}: #{super}"
end
end
module Trackable
def process
"Tracked: #{super}"
end
end
class DataProcessor
include Loggable
include Timestamped
include Trackable
end
processor = DataProcessor.new
puts "DataProcessor.ancestors: #{DataProcessor.ancestors[0..4]}"
puts "processor.process: #{processor.process}"
Multiple modules create a stack where the most recently included appears first in lookup. Each module's process
method enhances the previous one using super
.
Run this to see the module stacking behavior:
ruby module_inclusion.rb
DataProcessor.ancestors: [DataProcessor, Trackable, Timestamped, Loggable, Object]
processor.process: Tracked: 2025-09-16 11:11:44 +0200: Processing...
Now add an example showing the difference between include
and prepend
:
# Previous code...
module Auditable
def save
"Audited: #{super}"
end
end
class Document
def save
"Saving document"
end
end
class IncludedDocument < Document
include Auditable
end
class PrependedDocument < Document
prepend Auditable
end
puts "IncludedDocument.ancestors: #{IncludedDocument.ancestors[0..3]}"
puts "PrependedDocument.ancestors: #{PrependedDocument.ancestors[0..3]}"
puts "IncludedDocument.new.save: #{IncludedDocument.new.save}"
puts "PrependedDocument.new.save: #{PrependedDocument.new.save}"
With include
, the module appears after the class, so the class method runs first. With prepend
, the module appears before the class and can wrap the original method.
Run the complete example:
ruby module_inclusion.rb
DataProcessor.ancestors: [DataProcessor, Trackable, Timestamped, Loggable, Object]
processor.process: Tracked: 2025-09-16 11:12:07 +0200: Processing...
IncludedDocument.ancestors: [IncludedDocument, Auditable, Document, Object]
PrependedDocument.ancestors: [Auditable, PrependedDocument, Document, Object]
IncludedDocument.new.save: Audited: Saving document
PrependedDocument.new.save: Audited: Saving document
The key difference is positioning: Auditable
appears after IncludedDocument
but before PrependedDocument
. This determines which method gets called first and enables different enhancement patterns.
Introspecting the object model
Ruby provides extensive introspection capabilities that let you examine the object model at runtime. These tools help you understand method resolution, debug issues, and build metaprogramming solutions that work with Ruby's object system rather than against it. Learning to use these introspection methods effectively makes you a more capable Ruby developer.
The key introspection methods reveal different aspects of the object model. Methods like method
, source_location
, and owner
help you trace where methods are defined and how they're resolved. Others like respond_to?
and methods
help you understand what an object can do without necessarily calling methods on it.
These capabilities become particularly valuable when working with complex inheritance hierarchies, dynamic method definition, or debugging method resolution issues. They provide a window into Ruby's internal decision-making process during method lookup.
Create a comprehensive introspection example:
module Serializable
def to_json
"JSON representation"
end
end
class Model
def save
"Saving to database"
end
end
class User < Model
include Serializable
def save
"User-specific save logic"
end
end
user = User.new
def user.custom_method
"Instance-specific behavior"
end
puts "User.ancestors: #{User.ancestors[0..4]}"
# Examine specific methods
[:save, :to_json, :custom_method].each do |method_name|
method_obj = user.method(method_name)
puts "#{method_name} defined in: #{method_obj.owner}"
end
This demonstrates where Ruby finds each method during lookup, revealing the actual source of method definitions in complex hierarchies.
Run this to see detailed method information:
ruby introspection.rb
User.ancestors: [User, Serializable, Model, Object, Kernel]
save defined in: User
to_json defined in: Serializable
custom_method defined in: #<Class:#<User:0x000000010266b7c0>>
Now add methods for exploring object capabilities:
# Previous code...
puts "\nExploring object capabilities:"
puts "Public methods count: #{user.methods.length}"
puts "Singleton methods: #{user.singleton_methods}"
puts "Can respond to 'save'? #{user.respond_to?(:save)}"
puts "Can respond to 'puts' (private)? #{user.respond_to?(:puts, true)}"
# Find methods from specific classes/modules
user_methods = user.methods.select do |method_name|
[User, Serializable].include?(user.method(method_name).owner)
end
puts "Methods from User/Serializable: #{user_methods}"
Ruby's introspection tools help you understand method resolution and debug complex inheritance scenarios by showing exactly where methods are defined and what an object can do.
Run the complete introspection example:
ruby introspection.rb
User.ancestors: [User, Serializable, Model, Object, Kernel]
save defined in: User
to_json defined in: Serializable
custom_method defined in: #<Class:#<User:0x0000000102e5b610>>
Exploring object capabilities:
Public methods count: 54
Singleton methods: [:custom_method]
Can respond to 'save'? true
Can respond to 'puts' (private)? true
Methods from User/Serializable: [:save, :to_json]
The results reveal exactly where methods are defined and what capabilities an object has, making introspection essential for debugging complex method resolution scenarios.
Final thoughts
Ruby's object model creates a sophisticated yet predictable system for organizing behavior and resolving method calls. The method lookup chain follows a clear order: singleton methods, included modules (in reverse order of inclusion), the class itself, then up the inheritance hierarchy. Understanding this sequence helps you predict method resolution and design effective class hierarchies.
Ruby's object model balances power with predictability. While the system supports sophisticated metaprogramming patterns, it maintains consistent rules that make behavior understandable. Explore the Ruby Object documentation and experiment with the introspection methods to deepen your understanding of how Ruby organizes and resolves object behavior.