Ask almost any developer how object-oriented programming works, and they will describe the same textbook picture: an object bundles data and methods together into a single unit.
In Ruby, you hear this idea reinforced constantly under the slogan: "Everything is an object."
You create a string, call .upcase on it, and it feels completely natural to assume that the string object owns that method. You create a user object, call .save, and assume .save lives inside your user.
Except that is not how Ruby works at all.
In Ruby, objects contain zero methods.
If you inspect an instance down at the C source code level in CRuby (MRI), you discover something surprising: an object stores its instance variables, a set of internal flags, and a pointer to another structure. It does not contain a single line of method code, nor does it contain a method table.
Where do methods actually live? How does Ruby know what to execute when you type user.greet? And what actually happens when you define a method on a single specific object?
To truly master Ruby, you have to look beneath the syntactic sugar and understand the mechanics of Ruby's object model: method tables, the hidden eigenclass, and the ancestry ladder.
The First Surprise: Objects Store State, Classes Store Methods
Let us look at a simple Ruby class:
class User
def initialize(name, email)
@name = name
@email = email
end
def greet
"Hello, I am #{@name}"
end
end
user = User.new("Alice", "[email protected]")
When Ruby evaluates User.new, the virtual machine allocates a heap structure called an RObject.
Inside this RObject, Ruby stores two things:
- An array of instance variables:
@nameset to"Alice", and@emailset to"[email protected]". - An internal pointer named
klass, pointing directly to theUserclass object.
Notice what is missing from user: the greet method is nowhere to be found.
Methods in Ruby live exclusively inside classes (represented internally as RClass).
The User class contains a hash table called m_tbl (the method table). When you define def greet, Ruby writes an entry into User's method table mapping the symbol :greet to its compiled bytecode body.
When you call user.greet:
- Ruby does not look inside
user. - Ruby follows
user's internalklasspointer to theUserclass. - Ruby searches
User'sm_tblfor:greet. - It finds the method and executes it in the context of
self = user.
This separation is what makes Ruby memory-efficient. You can instantiate one hundred thousand User objects, and all one hundred thousand share the exact same method table inside User. The instances only hold their personal instance variables.
The Mystery of Singleton Methods
Now consider this famous Ruby feature: you can add a method to just one individual object without affecting any other instance of that class.
str1 = "hello"
str2 = "world"
def str1.shout
upcase + "!!!"
end
str1.shout #=> "HELLO!!!"
str2.shout #=> NoMethodError: undefined method 'shout' for "world":String
This raises a fascinating puzzle:
- We already know that objects cannot hold methods. So
:shoutcannot live insidestr1. -
:shoutcannot live insideString, because thenstr2would also have access to it.
If objects cannot hold methods, and the class cannot hold this method, where on earth does shout live?
Ruby solves this by doing something brilliant: it quietly invents an invisible class on the fly.
The moment you define a method on str1, Ruby creates an anonymous, hidden class called an eigenclass (officially referred to as a singleton class).
Ruby then performs an architectural sleight of hand:
- It intercepts
str1's internalklasspointer and repoints it to this new singleton class (#<Class:str1>). - It sets the superclass of the singleton class to
String. - It places the
:shoutmethod inside the singleton class's method table.
When you call str1.shout, Ruby follows the klass pointer into #<Class:str1>, finds :shout, and runs it.
When you call str1.upcase, Ruby looks in #<Class:str1>, does not find :upcase, follows the superclass pointer up to String, finds :upcase, and runs it.
To regular code, the singleton class is almost completely transparent. You do not see it in str1.class (which still reports String). But you can prove it exists with Ruby's reflection methods:
str1.singleton_class
#=> #<Class:#<String:0x00007fffa80c2180>>
str1.singleton_methods
#=> [:shout]
"Class Methods" Do Not Exist
Once you understand eigenclasses, one of the biggest myths in Ruby evaporates: there is no such thing as a "class method".
Developers frequently write code like this:
class Post
def self.recent
# query recent posts
end
end
Tutorials often say: "This defines a class method on Post."
In languages like Java or C++, a static method is a distinct language construct. But in Ruby, Post is not a special entity. Post is simply an object. It is an instance of the class Class.
Post.class
#=> Class
Because Post is an object, writing def Post.recent or def self.recent is doing the exact same thing as writing def str1.shout: it is defining a singleton method on an object.
The :recent method does not live in Post, and it does not live in Class. It lives in Post's own private eigenclass (#<Class:Post>).
In Ruby, there are only instance methods. What we call "class methods" are merely instance methods living on the singleton class of a class object. The entire language operates on one single, unified rule.
The Complete Method Lookup Ladder
When you type receiver.message, what is the exact order Ruby follows to find the code?
Ruby does not search haphazardly. It walks an exact, deterministic ladder called the ancestors chain. The first match it encounters executes immediately.
Here is the exact step-by-step lookup path:
- The Receiver's Eigenclass: If the object has any singleton methods, they get first priority.
-
Prepended Modules on the Eigenclass: Any module added with
prependsits in front of the eigenclass. - The Receiver's Class: The standard class where ordinary instance methods reside.
-
Prepended Modules on the Class: If a module was
prepended to the class, its methods override the class's own methods. -
Included Modules on the Class: If a module was
included in the class, it is inserted directly above the class in the hierarchy. - Superclasses (and their modules): Ruby climbs to the parent class, checking its prepends, own methods, and includes.
- Object, Kernel, and BasicObject: The root ancestors shared by virtually all Ruby objects.
What Happens When Nothing Matches?
If Ruby climbs all the way to BasicObject without finding a matching method, it does not immediately raise an error.
Instead, Ruby walks back down to the starting receiver and starts the entire search again from step one, this time searching for a method named method_missing.
Because Kernel provides a default implementation of method_missing that raises NoMethodError, your program only crashes if no intermediate module or class intercepts it.
This two-pass search is what powers Ruby's famous metaprogramming libraries, from ActiveRecord dynamic finders to OpenStruct.
How "Everything Is an Object" Runs Fast: Tagged Pointers
If every number, boolean, and nil value in Ruby is an object with methods, how does Ruby avoid grinding to a halt allocating heap memory for basic math?
If you run:
total = 0
1_000_000.times { |i| total += i }
Does Ruby allocate one million RObject structs on the heap?
In CRuby, the answer is no, thanks to an internal optimization called Tagged Pointers (Immediate Values).
On a 64-bit operating system, memory addresses are aligned to 8-byte boundaries. That means the lowest 3 bits of any valid heap pointer are always 000.
CRuby takes advantage of these unused bits. When a 64-bit value represents a small integer (called a Fixnum), Ruby shifts the integer left by one bit and sets the lowest bit to 1:
value = (integer << 1) | 1
When the Ruby virtual machine sees a value with the lowest bit set to 1, it knows: "This is not a heap pointer. This is the raw integer itself."
Similarly, CRuby reserves specific bit patterns for singleton values:
-
falseis represented as0x00 -
nilis represented as0x08 -
trueis represented as0x14
When you call 42.even?, Ruby does not inspect a heap block. It detects the tagged pointer, looks up the method in the Integer class method table, and executes the code without a single byte of memory allocation or garbage collection overhead.
You get the clean conceptual purity of "everything is an object" combined with the efficiency of primitive register arithmetic.
The Mental Shift
Ruby's object model is often described as complex, but under the surface, it is remarkably minimal:
-
Objects store state and an internal
klasspointer. They never hold methods. -
Classes store methods and an internal
superpointer. They never hold instance state. - Singleton methods live in invisible eigenclasses, seamlessly spliced into the lookup chain.
- Class methods are just singleton methods on class objects, because classes are objects too.
- Tagged pointers preserve pure object semantics without sacrificing performance.
Once you see the object model through this lens, Ruby metaprogramming stops feeling like black magic. Concepts like class_eval, instance_eval, prepend, and singleton classes become predictable mechanical tools that follow one coherent set of rules.
When you first learned Ruby, what was the quirk in its object model that made you stop and rethink how programming languages work? And if you have used eigenclasses in production, what was the problem you solved with them?
References:
- Ruby Core Documentation: Class and Object
- Ruby Core Documentation: Object#singleton_class
- Pat Shaughnessy: Ruby Under a Microscope (In-depth analysis of MRI C structures)
- Paolo Perrotta: Metaprogramming Ruby 2 (The definitive guide to Ruby's object model)




