The Decorator Pattern in Ruby

· 5 min read · Updated March 31, 2026 · intermediate
ruby decorator-pattern design-patterns solid guides

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 is composition over inheritance — one of the SOLID principles. 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.

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.

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.

You can also pass any object to SimpleDelegator.new without subclassing:

require "delegate"

price = SimpleDelegator.new(100)
price.to_s        # => "100" (delegated to Integer)

Using 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.

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.

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.

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.

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.

See Also