rubyguides

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:

  1. Holds a reference to the wrapped component
  2. Implements the same interface (cost, description)
  3. Delegates to the wrapped component
  4. 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.

DecoratorModule Mixin
Adds behaviour atRuntime (per instance)Class level (all instances)
Can be stackedYes, in any orderNo — single inclusion
Requires interface matchingYesNo — methods merge into class
Removes cleanlyYes — unwrap to get originalTricky — 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