When I first started working with Rails, I saw include everywhere. Models included modules, controllers included concerns, and some classes used extend or prepend. At first, the differences seemed simple:
- include adds instance methods.
- extend adds class methods.
- prepend overrides existing methods.
But the more I worked with Rails, the more I realized that memorizing these definitions wasn't enough. The important question is: How does Ruby decide which method to execute when multiple classes and modules define the same method?
Once we understand that, a lot of Rails magic becomes much easier to explain.
1. Why do we need modules in the first place?
Imagine we have two models, User and Order, that both need to normalize an email address. Without a shared module, we might write:
// language: ruby
class User < ApplicationRecord
def normalized_email
email.strip.downcase
end
end
class Order < ApplicationRecord
def normalized_email
email.strip.downcase
end
endThe implementation is small, but duplicated code tends to grow. If the normalization rules change, we have to remember to update both places. We could introduce a base class, but
// language: ruby
module EmailNormalizable
def normalized_email
email.strip.downcase
end
endNow both models can reuse the behavior:
// language: ruby class User < ApplicationRecord include EmailNormalizable end class Order < ApplicationRecord include EmailNormalizable end
We can call the method on either instance:
User.new(email: " [email protected] ").normalized_email # => "[email protected]" Order.new(email: " [email protected] ").normalized_email # => "[email protected]"
The interesting part is that Ruby doesn't need to copy the method into both classes. It changes the method lookup chain so that Ruby knows where to find the method. That distinction becomes important when we introduce
2. Include: Add behavior to instances
Let's start with the most common case.
// language: ruby
module Timestampable
def formatted_timestamp
created_at.strftime("%Y-%m-%d")
end
end
class Post < ApplicationRecord
include Timestampable
endBecause
// language: ruby post = Post.first post.formatted_timestamp
We can verify the relationship:
// language: ruby Post.ancestors
The relevant part of the result looks like this:
// language: ruby [ Post, Timestampable, ApplicationRecord, # ... ]
When Ruby receives:
// language: ruby post.formatted_timestamp
it searches for formatted_timestamp in the method lookup chain, starting with the object's class.
Conceptually:
// language: ruby
class Post < ApplicationRecord
include Timestampable
def formatted_timestamp
"Custom format"
end
end
Post.new.formatted_timestamp
# => "Custom format"The class's own implementation takes precedence over the included module.
The mental model: include adds reusable instance behavior to a class without requiring an inheritance relationship between the classes that use it.
3. Extend: Add behavior to an object
Now suppose we want a class-level method rather than an instance method. For example, we want User.normalize_email(email) .
We can define a module:
// language: ruby
module EmailNormalizer
def normalize_email(email)
email.strip.downcase
end
endThen use extend:
// language: ruby
class User < ApplicationRecord
extend EmailNormalizer
end
User.normalize_email(" [email protected] ")
# => "[email protected]"This also works outside a class definition:
// language: ruby
user = User.new
user.extend(EmailNormalizer)
user.normalize_email(" [email protected] ")
# => "[email protected]"Only that particular object has been extended. We haven't added the method to every User instance.
4. Prepend: Change the method lookup order
This is where things become more interesting. Suppose we have a payment service:
// language: ruby
class PaymentService
def process
"Payment ID"
end
endNow we want to add logging around process without modifying the original implementation. One possible solution is to create a module:
// language: ruby
module PaymentLogger
def process
puts "Starting payment..."
result = super
puts "Payment completed."
result
end
endIf we use
// language: ruby
class PaymentService
include PaymentLogger
def process
"Payment ID"
end
endCalling process executes the class's own method first. The logger's method won't automatically run. Instead, we can use prepend:
// language: ruby
class PaymentService
prepend PaymentLogger
def process
"Payment ID"
end
end
PaymentService.new.process
# => Starting payment...
# => Payment completed.
# => Payment IDWhy does it work? Because prepend places the module before the class in the method lookup chain.
// language: ruby PaymentService.ancestors
The relevant order is:
[ PaymentLogger, PaymentService, # ... ]
When Ruby receives
// language: ruby result = super
tells Ruby to continue searching for the next implementation of process in the lookup chain. In this case, that is PaymentService#process.
This is the key difference between include and prepend . With include , the class's own method comes first. With prepend , the module's method comes first. prepend is useful when we want to wrap existing behavior, such as adding instrumentation, logging, or compatibility layers, while preserving the original implementation through super.
5. What about extend self and module_function?
There is another pattern that often causes confusion:
// language: ruby
module EmailNormalizer
extend self
def normalize(email)
email.strip.downcase
end
end
EmailNormalizer.normalize(" [email protected] ")
# => "[email protected]"So why do we sometimes see
Because they're operating on different objects. In the first example:
// language: ruby class User < ApplicationRecord extend EmailNormalizer end
we add EmailNormalizer 's methods to the User class object. In the second example:
// language: ruby module EmailNormalizer extend self end
we extend the EmailNormalizer module object with its own instance methods. This allows the module to expose those methods directly. Another option is module_function :
// language: ruby
module EmailNormalizer
module_function
def normalize(email)
email.strip.downcase
end
endThis makes normalize callable as a module method and makes the instance-method version private.
6. ActiveSupport::Concern in Rails
In Rails, we often use concerns to share behavior between models or controllers. For example, both Post and Product need a method to check whether a record is published. Instead of duplicating the method, we can extract it into a concern:
// language: ruby
module Publishable
extend ActiveSupport::Concern
def published?
published_at.present?
end
class_methods do
def published
where.not(published_at: nil)
end
end
endThen include it in our models:
// language: ruby class Post < ApplicationRecord include Publishable end class Product < ApplicationRecord include Publishable end
Now both models can use the same behavior:
// language: ruby post.published? Post.published product.published? Product.published
The interesting part is
In this example, published? is an instance method, while published is a class method.
The mental model:
7. A common Rails mistake: Using concerns for everything
Imagine a model that looks like this:
// language: ruby class Order < ApplicationRecord include Searchable include Exportable include Notifiable include Auditable include StateManagement include PaymentHandling end
At first glance, this seems nicely organized. Each responsibility lives in a separate file. But when we call:
// language: ruby order.process
Where does process come from?
Does Order define it? Does PaymentHandling define it? Does another included module override it? Does one of those methods call super?
As the number of modules grows, understanding the behavior can become harder than reading a single class. This is why I wouldn't treat concerns as a universal solution to large models.
Before extracting a module, I would ask:
- Does this behavior represent a distinct responsibility?
- Is it genuinely reusable, or am I only moving code to another file?
- Does the module depend on undocumented methods or instance variables from the host class?
- Could a dedicated service object or another explicit abstraction express the relationship more clearly?
Modules are great for sharing behavior, but they can also introduce hidden dependencies.
A small, cohesive concern is usually easier to understand than a large concern that knows too much about the class including it.
The mental model
After understanding these mechanisms, I think the easiest way to remember them is to focus on where the methods enter the lookup chain.
include : add module behavior behind the class's own methods.extend : add module behavior to a particular object, often a class object.prepend : add module behavior before the class's own methods, making it useful for wrapping existing implementations with super.



