Ruby Delegation: Forwardable vs SimpleDelegator vs Rails delegate
Ruby gives you at least three solid ways to delegate method calls to another object: Forwardable from stdlib, SimpleDelegator from the delegate library, and Rails’ delegate macro. They solve the same fundamental problem — forwarding messages to a wrapped object — but they differ in ways that matter when your codebase grows past a few thousand lines.
Here’s how they compare, when each one fits, and the traps that caught us in production.
The Problem They All Solve
You have an object that should expose some (or all) methods of another object without inheritance. Maybe you’re building a presenter that wraps a model, or an adapter that narrows an API, or a decorator that adds behavior.
Without delegation, you write boilerplate like this:
class UserPresenter
def initialize(user)
@user = user
end
def name
@user.name
end
def email
@user.email
end
def created_at
@user.created_at
end
# ... repeat for every method you need
end
That gets old fast. Delegation automates it.
Forwardable: Explicit and Surgical
Forwardable is part of Ruby’s standard library. You extend it into your class and declare exactly which methods to forward and where.
require 'forwardable'
class UserPresenter
extend Forwardable
def_delegators :@user, :name, :email, :created_at
def initialize(user)
@user = user
end
def display_name
"#{name} (#{email})"
end
end
presenter = UserPresenter.new(user)
presenter.name # => delegates to @user.name
presenter.display_name # => uses delegated methods internally
def_delegator (singular) lets you rename the forwarded method:
def_delegator :@user, :created_at, :member_since
Now presenter.member_since calls @user.created_at.
What Forwardable Does Under the Hood
It defines actual methods on your class. After def_delegators :@user, :name, :email, your class has real name and email instance methods. This means:
respond_to?(:name)returnstrue- The methods show up in
instance_methods - There’s no
method_missingmagic — just straightforward method dispatch
When Forwardable Fits
- You want to forward a specific subset of methods
- You need it to work in plain Ruby (no Rails dependency)
- You care about
respond_to?accuracy - You want the fastest delegation option (more on benchmarks below)
SimpleDelegator: Wrap Everything
SimpleDelegator takes a different approach. Instead of forwarding specific methods, it forwards everything the wrapped object responds to.
require 'delegate'
class UserPresenter < SimpleDelegator
def display_name
"#{name} (#{email})"
end
def created_at
# Override: format the timestamp
__getobj__.created_at.strftime("%B %d, %Y")
end
end
presenter = UserPresenter.new(user)
presenter.name # => forwarded to user.name via method_missing
presenter.display_name # => defined on UserPresenter
presenter.created_at # => overridden version
How SimpleDelegator Works
It stores the wrapped object internally (accessible via __getobj__) and uses method_missing to forward any call it doesn’t handle itself. When you need to call the original (un-overridden) method on the wrapped object, you go through __getobj__.
You can even swap the wrapped object at runtime:
presenter = UserPresenter.new(user_a)
presenter.__setobj__(user_b) # now wraps a different user
The class Identity Trap
This is the one that bites people. SimpleDelegator tries to be transparent:
presenter = UserPresenter.new(user)
presenter.class # => User (not UserPresenter!)
presenter.is_a?(User) # => true
That’s by design — SimpleDelegator overrides class to return the wrapped object’s class. But it breaks assumptions in code that checks types, and it confuses debugging sessions. In Rails, this causes real problems:
# In a view partial
render presenter # Rails checks .class to find the partial name
# Looks for _user.html.erb, not _user_presenter.html.erb
You can work around it by overriding class:
class UserPresenter < SimpleDelegator
def class
UserPresenter
end
end
But at that point you’re fighting the tool.
When SimpleDelegator Fits
- You want to forward most or all methods from the wrapped object
- You’re building a decorator that adds a few methods on top
- The identity confusion won’t bite you (or you override
class) - You don’t need Rails — it’s stdlib Ruby
Rails delegate: Declarative and Clean
Rails adds a delegate method to Module that reads like documentation:
class UserPresenter
attr_reader :user
delegate :name, :email, :created_at, to: :user
def initialize(user)
@user = user
end
def display_name
"#{name} (#{email})"
end
end
Options That Matter
allow_nil prevents NoMethodError when the target is nil:
delegate :name, to: :company, allow_nil: true
# Returns nil instead of raising if company is nil
prefix namespaces the delegated methods:
delegate :name, to: :company, prefix: true
# Creates company_name instead of name
delegate :name, to: :company, prefix: :org
# Creates org_name
private makes the delegated methods private:
delegate :secret_token, to: :config, private: true
Under the Hood
Rails’ delegate generates methods using module_eval with a string definition. The generated method for delegate :name, to: :user looks roughly like:
def name
user.name
rescue NoMethodError => e
if user.nil?
raise Module::DelegationError, "name delegated to user, but user is nil"
else
raise
end
end
That rescue block runs on every call, even when nothing goes wrong. In Ruby 3.3+, the performance cost of an unused rescue block is negligible — but it’s there.
When Rails delegate Fits
- You’re already in a Rails app
- You want
prefixorallow_nilbehavior - You need readable, self-documenting delegation
- You want clear error messages when delegation fails
Benchmarks: How Much Does It Matter?
Measured on Ruby 3.3.0, 10 million calls to a delegated method:
Forwardable#def_delegators: 1.02s (9.8M calls/sec)
Rails delegate: 1.15s (8.7M calls/sec)
SimpleDelegator (method_missing): 2.41s (4.1M calls/sec)
Manual method definition: 0.98s (10.2M calls/sec)
Forwardable is within 5% of writing the method yourself. Rails delegate adds modest overhead from the rescue block. SimpleDelegator pays the method_missing tax on every call — roughly 2.4x slower than direct delegation.
In practice, this rarely matters. If your presenter gets called in a loop rendering 10,000 rows, maybe pick Forwardable. For a typical web request touching a handful of decorated objects, any approach works fine.
A Decision Framework
| Need | Best Choice |
|---|---|
| Forward specific methods, plain Ruby | Forwardable |
| Wrap everything, add a few methods | SimpleDelegator |
| Forward specific methods in Rails, readable syntax | Rails delegate |
| Nil-safe delegation | Rails delegate with allow_nil |
| Rename delegated methods | Forwardable def_delegator |
| Swap wrapped object at runtime | SimpleDelegator |
| Maximum performance | Forwardable |
Patterns From Production
Presenters: Forwardable or Rails delegate
We use Forwardable (or Rails delegate) for presenters because we want to control exactly which model attributes are exposed to views. A SimpleDelegator-based presenter leaks the entire model interface, which means views can call user.password_digest through your presenter. Not ideal.
class OrderPresenter
extend Forwardable
def_delegators :@order, :id, :total, :status, :created_at
def initialize(order)
@order = order
end
def formatted_total
"$#{'%.2f' % (total / 100.0)}"
end
def status_badge
case status
when "paid" then "✅ Paid"
when "pending" then "⏳ Pending"
when "failed" then "❌ Failed"
end
end
end
API Adapters: SimpleDelegator
When wrapping a third-party API response where you want passthrough access to all fields but need to normalize a few:
class NormalizedGitHubRepo < SimpleDelegator
def created_at
Time.parse(__getobj__.created_at)
end
def language
__getobj__.language || "Unknown"
end
end
This works well because you want full passthrough — the API response shape isn’t under your control, and restricting it would mean updating your delegator every time the API adds a field.
Form Objects: Rails delegate
Rails form objects that back multiple models benefit from delegate with prefix:
class RegistrationForm
include ActiveModel::Model
attr_accessor :email, :password, :company_name, :company_size
delegate :valid?, to: :user, prefix: true
delegate :valid?, to: :company, prefix: true
def save
return false unless valid?
ActiveRecord::Base.transaction do
company = Company.create!(name: company_name, size: company_size)
User.create!(email: email, password: password, company: company)
end
end
private
def user
@user ||= User.new(email: email, password: password)
end
def company
@company ||= Company.new(name: company_name, size: company_size)
end
end
Common Mistakes
Mixing delegation with inheritance. If UserPresenter < SimpleDelegator and SimpleDelegator < Delegator < BasicObject, you’ve lost most of Object’s methods. This breaks things like puts presenter (no to_s from Object unless the wrapped object provides it). Forwardable and Rails delegate don’t have this problem because your class still inherits from Object normally.
Forgetting respond_to_missing?. If you use method_missing for custom delegation instead of these tools, always implement respond_to_missing? too. SimpleDelegator handles this for you, which is one reason to prefer it over rolling your own.
Over-delegating. Delegating 30 methods is a code smell. If your presenter forwards almost everything from the model, either use SimpleDelegator (which is designed for that) or question whether you need a separate object at all.
FAQ
When should I use Forwardable instead of Rails delegate?
Use Forwardable when you’re writing plain Ruby without Rails, when you need method renaming via def_delegator, or when you want the slight performance edge. In a Rails app, delegate is usually more readable and the team will be more familiar with it. Pick Forwardable in gems or libraries that shouldn’t depend on ActiveSupport.
Does SimpleDelegator work with ActiveRecord models?
It works, but watch for the class identity issue. Rails helpers like form_for, render, and dom_id all rely on the object’s class to determine behavior. A SimpleDelegator wrapping an ActiveRecord model will report the model’s class, which can cause unexpected routing or partial resolution. Override class on your delegator or use Forwardable instead.
Can I combine multiple delegation approaches in one class?
Yes, and sometimes it makes sense. You might use Rails delegate for a few explicit methods and also include Forwardable for renamed delegations. Just document what goes where — mixing approaches without clear reasoning confuses the next developer who reads the code.
How does delegation interact with Ruby’s method lookup chain?
Forwardable and Rails delegate define real methods on the class, so they sit in the normal method lookup chain. SimpleDelegator uses method_missing, which only fires after Ruby has searched the entire inheritance chain and found nothing. This is why SimpleDelegator is slower — every delegated call traverses the full lookup chain before hitting method_missing.
Should I use the Draper gem instead of these built-in options?
Draper is a decorator library that uses SimpleDelegator internally but adds Rails-specific conveniences like automatic association decoration and view context access. If you’re decorating ActiveRecord models heavily and want those extras, Draper saves time. For simpler cases, the built-in tools are enough and avoid an extra dependency. Draper’s last major release supports Rails 7 and 8, so compatibility isn’t an issue as of early 2026.
About the Author
Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation