The most familiar way to add behavior to an object is inheritance. But once several behaviors need to combine, inheritance quickly hits a wall. Consider adding milk, sugar, and whip to a coffee. Making a subclass for each combination — MilkCoffee, SugarCoffee, MilkSugarCoffee, MilkSugarWhipCoffee — explodes the class count combinatorially. Decorator solves this with composition instead of inheritance.

Shared Intent

Decorator has three intents.

  • Keep the same interface — a decorator implements the same interface as the thing it wraps. The client never distinguishes the original from a decorator.
  • Compose at runtime — which behaviors to add is decided at runtime, not compile time. Decorators wrap one another to form a combination.
  • Single responsibility — each decorator adds exactly one behavior. The combination is expressed by the order of wrapping.

The GoF Form

GoF defines four roles: Component, ConcreteComponent, Decorator, ConcreteDecorator.

Component is the common interface. ConcreteComponent is the original object, and Decorator implements Component while holding a reference to another Component inside. ConcreteDecorator adds its own behavior to that reference and delegates. Wrap, delegate, add — the structure nests recursively.

flowchart LR
    C["Whip"] --> B["Milk"]
    B --> A["Espresso"]
    A -.cost 3000.-> B
    B -.+ 500.-> C
    C -.+ 700.-> R["4200"]
interface Coffee {
    int cost();
}

class Espresso implements Coffee {
    public int cost() { return 3000; }
}

abstract class CoffeeDecorator implements Coffee {
    protected final Coffee inner;
    CoffeeDecorator(Coffee inner) { this.inner = inner; }
}

class Milk extends CoffeeDecorator {
    Milk(Coffee inner) { super(inner); }
    public int cost() { return inner.cost() + 500; }
}

new Milk(new Espresso()) is an espresso with milk. Nesting like new Whip(new Milk(new Espresso())) expresses the combination through the order of wrapping. Because each wrapper keeps the same Coffee interface, they can stack indefinitely.

Language Implementations

Java’s java.io is the textbook case. new BufferedInputStream(new FileInputStream(file)) adds buffering to a file stream. BufferedInputStream implements InputStream while wrapping another InputStream, so it keeps the interface intact and only adds behavior. Stacking several FilterInputStream types forms a decorator chain.

Python has two kinds of decorators, which is easy to confuse. The language’s @decorator syntax is a higher-order function that wraps a function. Its intent matches the GoF object Decorator, but its form differs.

def with_logging(func):
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@with_logging
def process(): ...

@with_logging wraps behavior at the function level. When you need an object-level Decorator, you write a separate wrapper class that satisfies the same interface. A function decorator works at the function level; a GoF Decorator works at the object level.

TypeScript expresses it by wrapping a class or through a middleware chain. Express middleware, passing the request object through layer after layer, is effectively Decorator: each middleware takes the request, adds behavior, and hands it to the next.

Distinguishing from Proxy and Adapter

All three wrap an object, but their intents differ.

  • Decorator — adds behavior. The interface stays the same.
  • Proxy — controls access. Lazy loading, permission checks, and caching are the goal, not adding behavior.
  • Adapter — converts an interface. The goal is to bridge two incompatible interfaces.

The wrapping shape is similar, but the problem each solves is not. Decorator is about behavior, Proxy about access, Adapter about compatibility.

Conclusion

Decorator replaces the subclass explosion of inheritance with composition. It stacks wrappers that keep the same interface to add behavior at runtime. The choice comes down to two questions.

  • Do the behavior combinations branch several ways and get decided at runtime? Then Decorator over inheritance.
  • Is the point of wrapping not adding behavior but controlling access or converting an interface? Then Proxy or Adapter.

Java’s java.io, Python’s function decorators, and web middleware chains are all variations on the same intent. What separates it from the similar-looking Proxy and Adapter is the problem being solved.

References

  • Factory — the same GoF series, on object creation
  • Builder — another case of a pattern absorbed into language features
  • GoF — Design Patterns: Elements of Reusable Object-Oriented Software (1994)