0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Include, Extend, Prepend in Rails: Understanding Ruby Modules

    Ruby on Rails
    Ruby on Rails
    published 2026-10-14 20:29:07 +0700 ·
    6 mins read
    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
    end

    The 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 User and Order don't necessarily represent the same kind of thing. They simply share a behavior. This is where Ruby modules become useful.
    // language: ruby
    module EmailNormalizable
      def normalized_email
        email.strip.downcase
      end
    end

    Now 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 extend and prepend.

    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
    end

    Because Timestampable is included in Post, instances of Post can call formatted_timestamp.
    // 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:  Post instance -> Post -> Timestampable -> ApplicationRecord. If Post defines the method itself, Ruby finds it before reaching Timestampable.
    // 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
    end

    Then 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
    end

    Now 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
    end

    If we use include:
    // language: ruby
    class PaymentService
      include PaymentLogger
    
      def process
        "Payment ID"
      end
    end

    Calling 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 ID

    Why 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 process, it finds the implementation in PaymentLogger first. Then:
    // 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 extend self instead of extend EmailNormalizer inside a class?
    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
    end

    This 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
    end

    Then 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 class_methods. With a regular module, include makes instance methods available to the class's instances. ActiveSupport::Concern provides class_methods as a convenient way to define methods available on the including class.
    In this example, published? is an instance method, while published is a class method.

    The mental model: ActiveSupport::Concern builds on Ruby modules and makes it easier to organize reusable behavior that integrates with Rails.

    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.