35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English

Ruby Delegation: Forwardable vs SimpleDelegator vs Rails delegate

TTB Software
ruby, rails
A practical comparison of Ruby's three main delegation approaches. When to use Forwardable, SimpleDelegator, or Rails delegate — with benchmarks, gotchas, and production patterns.

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) returns true
  • The methods show up in instance_methods
  • There’s no method_missing magic — 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 prefix or allow_nil behavior
  • 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.

T

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 Touch

Share this article

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