The Decorator Pattern in Ruby: Extending Objects at Runtime
The decorator pattern lets you add behaviour to objects without changing their class. Instead of subclassing, you wrap the original object inside a new object that intercepts calls and adds its own logic before or after delegating to the wrapped object.
This pattern shows up any time one object needs to stay small while another object adds polish, logging, caching, or formatting. That makes it a useful fit for serializers, presenters, caches, and wrappers around objects that already do the main job well. The wrapper stays focused on the extra concern, while the wrapped object keeps doing the core work.
Composition over inheritance, one of the SOLID principles, sits at the heart of this pattern. Instead of building a tall inheritance tree to add features, you stack decorators at runtime.
Simple delegation with Forwardable
The most lightweight way to forward method calls is Ruby’s Forwardable module. You declare which methods should pass through to the wrapped object.
require "forwardable"
class PlainFormatter
def format(data)
data.to_s
end
end
class UppercaseFormatter
extend Forwardable
def initialize(wrapped)
@wrapped = wrapped
end
def_delegator :@wrapped, :format
def format(data)
@wrapped.format(data).upcase
end
end
formatter = UppercaseFormatter.new(PlainFormatter.new)
formatter.format("hello")
# => "HELLO"
def_delegator creates a forwarding method. The first argument is the instance variable holding the wrapped object, and the second is the method name to call on it. You can override format to add behaviour while still delegating to the wrapped object for the base behaviour.
Forwardable works well when you only need to forward a handful of methods and you want to be explicit about it.
the decorator pattern, composition over inheritance
The classic decorator pattern in Ruby looks like this. Every decorator implements the same interface as the object it wraps, so decorators can be stacked in any order.
class Coffee
def cost
3
end
def description
"coffee"
end
end
class MilkDecorator
def initialize(component)
@component = component
end
def cost
@component.cost + 1
end
def description
"#{@component.description} + milk"
end
end
class SugarDecorator
def initialize(component)
@component = component
end
def cost
@component.cost + 0.5
end
def description
"#{@component.description} + sugar"
end
end
coffee = Coffee.new
coffee = MilkDecorator.new(coffee)
coffee = SugarDecorator.new(coffee)
coffee.cost # => 4.5
coffee.description # => "coffee + milk + sugar"
Each decorator:
- Holds a reference to the wrapped component
- Implements the same interface (
cost,description) - Delegates to the wrapped component
- Adds its own behaviour
This is the core idea. Everything else is a variation on how you avoid writing the delegation boilerplate.
The result is a small object that can be layered with other small objects. Because each wrapper speaks the same interface as the component underneath it, you can choose the order that best matches the work you need to do. That flexibility is what makes the pattern useful in real Ruby code, not just in diagrams.
Sometimes the wrapped object already exists and you only want to adjust one method. Other times you want to add several pieces of behavior, such as logging and caching, and each wrapper can stay responsible for exactly one concern. That keeps the code easy to reason about because every class has a narrow job.
Using SimpleDelegator
SimpleDelegator from the standard library eliminates the need to write delegation methods by hand. You subclass it and call super with the object to wrap.
Use it when most of the wrapped object’s interface should pass through unchanged. It is especially handy for presenters and decorators that only override one or two methods, because the rest of the object stays available without extra boilerplate.
require "delegate"
class TaxDecorator < SimpleDelegator
def cost
super + 2
end
end
class ShippingDecorator < SimpleDelegator
def cost
super + 5
end
end
item = Object.new
def item.cost; 10; end
def item.description; "widget"; end
item = TaxDecorator.new(item)
item = ShippingDecorator.new(item)
item.cost # => 17
item.description # => "widget" — automatically delegated
SimpleDelegator automatically forwards all method calls to the wrapped object unless you override them. This means you only define the methods you want to modify. Because the delegation is automatic, you can focus your subclass entirely on the behaviour you want to add instead of spending time wiring up pass-through methods for the parts you are not changing.
SimpleDelegator also works without subclassing when you only need the forwarding behaviour as-is. You can pass any Ruby object to SimpleDelegator.new and the resulting wrapper will transparently delegate every method call:
require "delegate"
price = SimpleDelegator.new(100)
price.to_s # => "100" (delegated to Integer)
That is a very small example, but it shows the main benefit clearly: you can start with an existing object and layer behavior on top of it without rewriting the original class. In practice, that makes decorators easy to drop into code that already has a working object graph.
instrumenting events with ActiveSupport::Notifications
When you need to observe and log method calls rather than modify their return values, ActiveSupport::Notifications provides an event system. This is a decorator that listens for events and acts on them without changing return values.
This is a better fit when the side effect is observational, not transformational. If you only want metrics, auditing, or timing data, a notification keeps the event separate from the business result and avoids coupling the two concerns together.
require "active_support/notifications"
class AuditDecorator < SimpleDelegator
def initialize(component, logger)
super(component)
@logger = logger
end
def cost
ActiveSupport::Notifications.instrument("cost.called", description: description) do
super
end.tap { |result| @logger.info "cost returned #{result}" }
end
private
def instrument_event(name, payload)
ActiveSupport::Notifications.instrument(name, payload)
end
end
This pattern is especially useful in Rails applications where you want to track performance or audit calls without cluttering the core business logic.
It also works well when multiple subscribers need the same event. One listener can log the request, another can store metrics, and a third can notify a monitoring service, all without changing the object that emitted the event in the first place.
building stackable decorators
The real power of decorators is that you can combine them in any order at runtime. This is called stacking.
class LoggingDecorator < SimpleDelegator
def initialize(component)
super(component)
@calls = []
end
def method_missing(meth, *args, &block)
@calls << [meth, args]
super
end
def call_log
@calls
end
end
class CachingDecorator < SimpleDelegator
def initialize(component)
super(component)
@cache = {}
end
def cost
@cache[:cost] ||= super
end
end
price = CachingDecorator.new(LoggingDecorator.new(Coffee.new))
price.cost
price.cost # served from cache
price.call_log # => [[:cost, []], [:cost, []]]
Notice that call_log only recorded two calls even though we called cost twice — the CachingDecorator intercepted the second call before it reached the LoggingDecorator. Order matters.
That ordering rule is the part people usually discover after they have already written a few wrappers. It is worth keeping in mind because the wrapper closest to the base object sees the original call first, while outer decorators see the wrapped result later. Pick the order that matches the behavior you want to preserve.
testing decorators
Decorators are easy to test in isolation because they have no dependencies on external state other than the wrapped object.
require "minitest/autorun"
class CachingDecoratorTest < Minitest::Test
def test_cost_returns_cached_value
component = Object.new
def component.cost; 5; end
decorator = CachingDecorator.new(component)
assert_equal 5, decorator.cost
assert_equal 5, decorator.cost # second call hits cache
end
def test_decorator_preserves_wrapped_interface
component = Object.new
def component.cost; 5; end
def component.description; "thing"; end
decorator = CachingDecorator.new(component)
assert_equal 5, decorator.cost
assert_equal "thing", decorator.description # not overridden
end
end
The key testing principle is that each decorator should be tested independently. Mock the wrapped component if needed, but don’t test the entire stack in a single test.
That keeps the test suite focused. A test for the caching wrapper should prove that caching works, while a test for the logging wrapper should prove that logging happens. The decorators can still be stacked in production, but the tests stay easier to understand when each layer has its own examples.
when to use decorators vs module mixins
Ruby gives you two ways to add behaviour to objects: decorators and module mixins.
| Decorator | Module Mixin | |
|---|---|---|
| Adds behaviour at | Runtime (per instance) | Class level (all instances) |
| Can be stacked | Yes, in any order | No — single inclusion |
| Requires interface matching | Yes | No — methods merge into class |
| Removes cleanly | Yes — unwrap to get original | Tricky — must use super everywhere |
Use a decorator when:
- You need to add behaviour to a specific instance at runtime
- You want to combine behaviours in different orders
- The base object comes from an external source you cannot modify
- You need to easily remove the decoration later
Use a module mixin when:
- The behaviour belongs to the class itself
- All instances of the class need the same behaviour
- You are building a framework or gem that extends core Ruby objects
# Module mixin — behaviour lives on the class
module Reportable
def generate_report
"Report for #{self.class}"
end
end
class Order
include Reportable
end
Order.new.generate_report # => "Report for Order"
# Decorator — behaviour is attached to a specific instance
class ExportDecorator < SimpleDelegator
def generate_report
"Export: #{super}"
end
end
ExportDecorator.new(Order.new).generate_report # => "Export: Report for Order"
The deciding question is: do you want this behaviour on all instances or just some instances? All instances means mixin. Some instances means decorator.
The decorator pattern in Ruby is most helpful when you need instance-level composition and clear boundaries between the base object and the extra behavior. If the behavior belongs on every instance, a mixin may be simpler. If it belongs on one wrapped instance, the decorator pattern is usually the cleaner choice.
See Also
- /guides/ruby-service-objects/ — Service objects complement decorators by encapsulating business logic that decorators then wrap and layer
- /guides/ruby-blocks-procs-lambdas/ — Procs and lambdas are the building blocks of flexible decorator behaviour
- /guides/ruby-dependency-injection/ — Decorators are a natural fit inside dependency-injected systems