The Decorator Pattern in Ruby — Clean Code Without the Bloat
by Eric Hanson, Backend Developer at Clean Systems Consulting
The problem decorators solve
You have a User model. The views need formatted output — full_name, avatar_url, membership_label. The naive solution is adding these to the model. Six months later the model has thirty presentation methods sitting next to database logic, validations, and business rules. The methods are tested alongside ActiveRecord callbacks and the test suite loads the entire Rails environment to verify string formatting.
The alternative is keeping the model clean and wrapping it in a presentation layer at the point of use. That's a decorator: an object that wraps another object, adds or overrides methods, and delegates everything else to the wrapped object. No subclassing. No callbacks. No model pollution.
SimpleDelegator — the stdlib baseline
Ruby ships SimpleDelegator in the standard library. It wraps an object and forwards any method call it doesn't define to the wrapped object via method_missing:
require 'delegate'
class UserPresenter < SimpleDelegator
def full_name
"#{first_name} #{last_name}".strip
end
def membership_label
case membership_tier
when "pro" then "Pro Member"
when "free" then "Free Tier"
else "Unknown"
end
end
def avatar_url
gravatar_url || "/images/default_avatar.png"
end
end
At the call site:
presenter = UserPresenter.new(@user)
presenter.full_name # => "Alice Nakamura"
presenter.email # => delegated to @user.email
presenter.membership_label # => "Pro Member"
The wrapped object is accessible via __getobj__ and replaceable via __setobj__. SimpleDelegator uses method_missing under the hood, so respond_to_missing? is also implemented correctly — presenter.respond_to?(:email) returns true even though UserPresenter doesn't define email.
The main limitation: presenter.is_a?(User) returns false. If anything in your call stack does type-checking — a serializer, a policy object, a method that calls user.is_a?(ActiveRecord::Base) — the decorator breaks that check. This is often acceptable in presentation layers, occasionally a hard blocker elsewhere.
Manual delegation — explicit and fast
SimpleDelegator is convenient but carries method_missing overhead and can obscure which methods are actually delegated. For decorators with a defined, bounded interface, explicit delegation is preferable:
class UserPresenter
def initialize(user)
@user = user
end
# Delegated methods — explicit
delegate :id, :email, :created_at, :membership_tier, to: :@user
def full_name
"#{@user.first_name} #{@user.last_name}".strip
end
def membership_label
case @user.membership_tier
when "pro" then "Pro Member"
when "free" then "Free Tier"
else "Unknown"
end
end
end
delegate here is ActiveSupport::CoreExt::Module::Delegation. Outside Rails, you replicate it manually or use Forwardable from the stdlib:
require 'forwardable'
class UserPresenter
extend Forwardable
def_delegators :@user, :id, :email, :created_at, :membership_tier
def initialize(user)
@user = user
end
end
Forwardable generates real methods rather than relying on method_missing, so delegation is as fast as a direct method call. The tradeoff: the delegation list is a maintenance surface. Add a method to the model that the presenter needs to expose and you have to update def_delegators explicitly. SimpleDelegator handles that automatically.
Module-based decoration without a wrapper class
For lightweight, additive behavior — logging, timing, feature flagging — you can decorate a specific object instance without a wrapper class using extend:
module TimedExecution
def call(...)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
result = super
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
Rails.logger.info "#{self.class}#call completed in #{elapsed.round(3)}s"
result
end
end
service = PlaceOrder.new(user: user, cart: cart, payment_method: pm)
service.extend(TimedExecution)
service.call
extend mixes the module into the object's singleton class, so only this instance is affected. Other PlaceOrder instances are untouched. super reaches the original call implementation because the module is prepended to the singleton class's ancestor chain.
This is the most surgical form of decoration Ruby offers. It's particularly useful in development or test environments where you want to instrument a specific object without modifying the class.
The downside: singleton class manipulation is implicit. Someone reading service.call has no idea TimedExecution is in play unless they see the extend call. For production instrumentation, prepend at the class level is more visible — the behavior applies uniformly and shows up in MyClass.ancestors.
Stacking decorators
One of the pattern's genuine benefits is composability. Multiple decorators can wrap the same object, each adding a layer:
class CachedUser < SimpleDelegator
def expensive_metric
Rails.cache.fetch("user_metric_#{id}", expires_in: 5.minutes) do
__getobj__.expensive_metric
end
end
end
class AuditedUser < SimpleDelegator
def update_role(role)
AuditLog.record(user_id: id, action: "role_change", to: role)
super
end
end
user = AuditedUser.new(CachedUser.new(User.find(id)))
The outer decorator's method calls fall through to the next layer and ultimately reach the User instance. Each decorator is independently testable with a stub at its inner boundary.
The thing to watch: decorator order matters when two layers intercept the same method. AuditedUser#update_role calling super reaches CachedUser#update_role if CachedUser defines it, not User#update_role directly. Explicit __getobj__ calls can bypass intermediate layers if you need to skip one — but if you need to bypass a decorator you stacked intentionally, the stack is probably wrong.
Where the pattern breaks down
Serializers and type checks. ActiveRecord serializers, JSON serializers (Blueprinter, Alba, fast_jsonapi), and policy objects often check object.class or object.is_a?. A decorator wrapping a User fails those checks. You can override class on the decorator:
def class
__getobj__.class
end
But that's a smell. If the system is doing type-based dispatch on your objects, a decorator that hides the type creates more problems than it solves. Use a presenter that's explicitly a different type and pass only what the serializer needs.
ActiveRecord associations. Decorating an AR model and then passing it to a method that calls user.posts or triggers additional queries works fine — the delegation reaches the underlying record. But saving, reloading, or touching the underlying record from inside the decorator couples presentation and persistence in a way that tends to cause confusion. Decorators should be read-only wrappers in most cases. If you need to write through a decorator, question whether the decorator is the right tool.
Draper. The Draper gem formalizes the Rails presenter/decorator pattern with decorate, decorates_association, and view context injection. It's a solid choice if you want conventions enforced across a team. The tradeoff is the added dependency and its opinions about how decorators relate to views. For non-Rails Ruby or simpler use cases, SimpleDelegator and Forwardable cover the same ground without the dependency.
The practical split
Use SimpleDelegator when you want full interface passthrough with minimal setup and the delegation boundary is open-ended.
Use Forwardable or delegate when you want explicit, documented delegation and performance matters.
Use extend on an instance when you need to add behavior to a single object at runtime without affecting the class.
Stack decorators when multiple orthogonal concerns — caching, auditing, presentation — need to wrap the same object independently.
The pattern earns its place when you find yourself adding methods to a model that exist only to serve one view, one API format, or one reporting context. That's the moment to wrap, not to pollute.