Decorator Pattern
Concept
Decorator attaches new behavior to an individual object at runtime by wrapping it in another object that implements the same interface. The wrapper (decorator) holds a reference to the wrapped instance, delegates every call it doesn't care about straight through, and adds its own behavior before and/or after delegating for the calls it does care about. Because the decorator implements the same interface as the thing it wraps, decorators can be stacked — a decorator wrapping a decorator wrapping the original object — and the caller at the outermost layer can't tell, and doesn't need to tell, how many layers of wrapping sit underneath. Each layer only knows about the one interface and the one object directly beneath it.
This is the pattern's whole reason to exist: subclassing can add behavior too, but subclassing commits to that behavior at compile/class-definition time and for every instance of the class. If a Coffee needs optional milk, sugar, and whipped cream, subclassing to cover every combination needs 2^3 = 8 subclasses (CoffeeWithMilk, CoffeeWithMilkAndSugar, ...), and every new optional extra doubles that count again. Decorator instead makes each extra its own wrapper class implementing the same Coffee interface, and combinations are built by composition at runtime — new WhippedCreamDecorator(new SugarDecorator(new MilkDecorator(coffee))) — with no combinatorial explosion of classes, and combinations chosen per-instance instead of baked into a class hierarchy.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Decorator (runtime wrapping) | Behaviors compose per-instance at runtime; no combinatorial subclass explosion; open/closed — new decorators don't touch existing code | Debugging a deep decorator stack means tracing a call through N wrapper layers; stack traces and "what actually handles this call" get harder to read as layers grow |
| Subclassing per combination | Simple to trace — one concrete class, one inheritance chain, easy to step through in a debugger | Combinations grow combinatorially with each new optional behavior; behavior is fixed per class, not swappable per-instance at runtime |
| Mixins / traits (where the language supports them) | Composition without runtime wrapping overhead; behavior resolved at class-definition time | Still fixed per class rather than per-instance; method resolution order across multiple mixins can itself become hard to reason about |
Configuration flags on one class (enableLogging, enableCaching) |
No wrapping, no extra objects, one class to read | Every optional behavior adds a conditional inside the core class, coupling unrelated concerns together and growing one method's cyclomatic complexity indefinitely |
The real-world instance of this pattern most engineers touch daily is HTTP middleware: an auth-check middleware wraps a logging middleware wraps a rate-limiter wraps the actual request handler, each one free to inspect/modify the request or response and decide whether to call the next layer at all. That "next layer" call is exactly a decorator's delegation to its wrapped object.
When to use / when not to
- Use when you need to add optional, combinable behavior to individual objects without touching the objects' own class — cross-cutting concerns like logging, caching, auth checks, retry logic, and input validation wrapped around an existing operation are the canonical case.
- Especially valuable when the set of possible combinations is large or grows over time (new middleware added to a request pipeline regularly) — Decorator adds a new capability as one new wrapper class, never modifying existing wrappers or the wrapped object.
- Don't reach for it when there's only ever one fixed combination of behaviors needed — a single
if enableLogging:check inside the core class is simpler and more debuggable than a wrapper class for a behavior that's never actually toggled per-instance. - Don't use it when callers need to inspect the concrete wrapped type or its identity — Decorator deliberately hides the wrapping, and code that needs
isinstance(obj, RawCoffee)to behave differently defeats the pattern's transparency guarantee.
Common pitfall
Decorators that don't faithfully implement the full wrapped interface — forwarding most methods but silently dropping or subtly altering the semantics of one — break the substitutability the pattern depends on. A caller holding a decorated object is supposed to be able to treat it exactly like the undecorated one; a decorator that, say, wraps a stream and forgets to forward close() correctly leaks a resource in exactly the code path that looks identical to the non-decorated case. The fix is discipline, not cleverness: every decorator should delegate every interface method by default and only override the ones it's actually adding behavior to, rather than hand-writing a full new implementation that's easy to leave incomplete.
Engineering Lens
Decorator is worth recognizing by its shape rather than its name: any place where "wrap this thing and add a step before/after calling through" shows up — HTTP middleware, ORM query wrappers, stream/IO wrappers (buffered, compressed, encrypted readers stacked on a raw file handle), Python's @decorator function syntax (a direct, first-class-function version of the same idea) — is this pattern, whether or not the code calls itself a "Decorator." The design question worth surfacing in review isn't whether to use it (middleware-shaped problems make the choice for you) but ordering: middleware order changes behavior (an auth check that runs after a rate limiter treats unauthenticated and authenticated traffic identically for rate-limiting purposes), and that ordering is easy to get wrong silently since every layer's interface looks the same from the outside.
Related
- Composite Pattern — both wrap objects behind a shared interface, but Composite's children are peers forming a tree while Decorator's wrapped object is a single instance being progressively layered
- Proxy — structurally similar (wraps an object behind the same interface) but controls access to the wrapped object rather than adding behavior to it