When extending Rails internals or modifying gem behavior, developers often reach for alias_method_chain patterns or monkey patching. These approaches create brittle code that breaks across upgrades. Module prepending offers a cleaner, more maintainable solution that Ruby has supported since version 2.0—yet many Rails developers underutilize it.
The Problem with Traditional Overrides
Consider a common scenario: adding audit logging to Active Record's save method. The naive approach looks like this:
# config/initializers/bad_audit.rb (DON'T DO THIS)
class ActiveRecord::Base
alias_method :original_save, :save
def save(*args, **options)
Rails.logger.info "Saving #{self.class.name}"
original_save(*args, **options)
end
endThis creates multiple problems: it pollutes the class with extra methods, breaks when other code uses the same pattern, and makes debugging a nightmare. Module prepending solves these issues elegantly.
How Prepend Works
When a module is prepended to a class, Ruby inserts it before the class in the method lookup chain. Calling super from the prepended module invokes the original class method. This creates a clean override without renaming or aliasing anything.
# config/initializers/audit_logging.rb
module AuditLogging
def save(*args, **options)
Rails.logger.info "[AUDIT] Saving #{self.class.name} id=#{id || 'new'}"
result = super
Rails.logger.info "[AUDIT] Save #{result ? 'succeeded' : 'failed'} for #{self.class.name}"
result
end
def destroy
Rails.logger.info "[AUDIT] Destroying #{self.class.name} id=#{id}"
super
end
end
ActiveSupport.on_load(:active_record) do
prepend AuditLogging
endThe ActiveSupport.on_load hook ensures the prepend happens after Active Record fully loads, avoiding load order issues. The super call passes through to the original save method naturally.
Targeted Prepending for Specific Models
Global prepending affects every model. For targeted behavior, prepend to specific classes or use a concern pattern:
# app/models/concerns/publishable.rb
module Publishable
extend ActiveSupport::Concern
included do
scope :published, -> { where.not(published_at: nil) }
scope :draft, -> { where(published_at: nil) }
end
# This module gets prepended to override save behavior
module SaveHooks
def save(*args, **options)
self.slug ||= title.parameterize if respond_to?(:slug) && respond_to?(:title)
super
end
end
class_methods do
def inherited(subclass)
subclass.prepend(SaveHooks)
super
end
end
included do
prepend SaveHooks
end
end
# app/models/article.rb
class Article < ApplicationRecord
include Publishable
validates :title, presence: true
validates :slug, uniqueness: true
endThis pattern keeps the prepend logic encapsulated within the concern. Any model including Publishable automatically gets the slug generation behavior.
Prepending to Controller Actions
Prepend works equally well for extending controller behavior. Here's a pattern for adding request timing to specific controllers:
# app/controllers/concerns/request_timing.rb
module RequestTiming
extend ActiveSupport::Concern
module ProcessActionTiming
def process_action(*args)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = super
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
response.headers['X-Request-Duration'] = "#{(duration * 1000).round(2)}ms"
Rails.logger.info "[TIMING] #{controller_name}##{action_name} took #{(duration * 1000).round(2)}ms"
result
end
end
included do
prepend ProcessActionTiming
end
end
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ApplicationController
include RequestTiming
skip_before_action :verify_authenticity_token
end
end
endEvery controller inheriting from Api::V1::BaseController now includes timing headers and logging without modifying action methods directly.
Extending Gem Behavior Safely
Prepending shines when modifying gem behavior without forking. Here's an example extending Solid Queue's job processing:
# config/initializers/solid_queue_extensions.rb
module SolidQueueJobExtensions
def perform
ActiveSupport::Notifications.instrument('solid_queue.job.perform', job: self.class.name) do
super
end
rescue StandardError => e
ErrorTracker.capture(e, context: { job_class: self.class.name, arguments: arguments })
raise
end
end
Rails.application.config.after_initialize do
if defined?(SolidQueue::Job)
SolidQueue::Job.prepend(SolidQueueJobExtensions)
end
endThe after_initialize hook ensures Solid Queue has loaded before attempting the prepend. The defined? check prevents errors if the gem isn't present in certain environments.
Prepend vs Include vs Extend
Understanding when to use each matters:
- include - Adds methods as instance methods, inserted after the class in lookup chain
- extend - Adds methods as class methods
- prepend - Adds methods as instance methods, inserted before the class in lookup chain
Use prepend when the goal is overriding existing behavior while maintaining access to the original via super. Use include for adding new behavior that doesn't need to wrap existing methods.
Common Mistakes to Avoid
Several pitfalls trip up developers new to prepending:
- Prepending multiple times - Each prepend adds another layer. Guard against double-prepending with
prepend Module unless ancestors.include?(Module) - Forgetting super - Omitting
supercompletely replaces the original method instead of wrapping it - Wrong load order - Prepending before the target class loads causes errors. Use
ActiveSupport.on_loadorafter_initializehooks - Argument mismatches - Always use
*args, **optionsor match the exact signature to avoid breaking when the original method signature changes
Debugging Prepend Chains
When behavior seems wrong, inspect the ancestor chain:
# rails console
Article.ancestors
# => [Article::SaveHooks, Article, Publishable, ApplicationRecord, ...]
# Find where a method is defined
Article.instance_method(:save).owner
# => Article::SaveHooksThe ancestor list shows prepended modules appearing before the class itself, confirming the override order.
Summary
Module prepending provides a robust pattern for extending Ruby and Rails classes. Unlike monkey patching or alias chains, prepend maintains clean inheritance semantics and plays well with other code. Use it for audit logging, timing instrumentation, gem extensions, and any scenario requiring method wrapping. The key rules: always call super to invoke the original, use proper load hooks to avoid timing issues, and guard against double-prepending in initializers that may reload.