AcademyDesign Patterns
Decorator pattern
Stacking behavior onto one object at a time without subclassing, the layers it costs to read through when debugging, and the fixed wrap order every decorator must respect.
6 min readUpdated 3 exercises
Decorator solves a problem inheritance handles badly: adding behavior to one specific object, optionally stacked with other behaviors, without a subclass for every combination.
A Decorator wraps an object in another object that implements the same interface, adds a bit of behavior, and forwards the rest. Wrap it again and the behaviors stack. The alternative, a subclass per combination, is what this pattern exists to avoid: CoffeeWithMilk, CoffeeWithMilkAndSugar, CoffeeWithMilkAndSugarAndCream, one subclass per combination, doubling every time a new topping ships.
The problem it solves
You have an HTTP handler. Some routes need logging, some need authentication, some need rate limiting, and some need all three.
With inheritance you would write LoggedHandler, AuthHandler, LoggedAuthHandler, RateLimitedAuthHandler, and so on. Three behaviors already need seven subclasses, and each new behavior doubles the count.
- Combinations explode. Every new behavior multiplies the classes.
- Behavior is fixed at compile time. You cannot add logging to one instance at runtime.
- Code is duplicated. The logging code is copied into every class that needs it.
The fix is to write each behavior once, as a wrapper, and combine wrappers as needed for each object.
Structure
Three parts:
- The component interface (
cost(),describe()): shared by the original and the wrappers. - The concrete component (
Coffee): the object being decorated. - Decorators (
MilkDecorator): hold a reference to another component of the same interface and add behavior before or after forwarding.
class Coffee {
cost() { return 2; }
describe() { return 'Coffee'; }
}
class MilkDecorator {
constructor(coffee) { this.coffee = coffee; }
cost() { return this.coffee.cost() + 0.5; }
describe() { return this.coffee.describe() + ' + milk'; }
}
// new MilkDecorator(new MilkDecorator(new Coffee())) → cost 3, "Coffee + milk + milk"Each layer only knows about the layer directly inside it. MilkDecorator never needs to know whether it's wrapping a bare Coffee or three other decorators already. That's what lets them stack in any order and any quantity without a combinatorial explosion of subclasses.
Where it is used in real systems
| Use case | What the decorator adds |
|---|---|
| HTTP middleware | Logging, authentication, CORS, rate limiting around a handler. |
| Caching | A cache layer in front of a function or repository. |
| Retries and timeouts | Wrapping a network call with retry logic. |
| I/O streams | Buffering, compression or encryption around a stream. |
| UI components | A border, a scrollbar or a tooltip around a widget. |
| Permissions | A check wrapped around an action. |
| Metrics and tracing | Timing and counters around any operation. |
| Feature flags | Enabling extra behavior for some users only. |
The common feature: a core operation stays simple, and cross-cutting concerns are layered around it, one at a time.
Decorators you already use
- Express, Koa and Fastify middleware. Each function wraps the next one, running code before and after it. Chaining
app.use(...)is stacking decorators. - Higher-order functions. A
withRetry(fn)ormemoize(fn)returns a new function with the same signature and extra behavior. - React higher-order components and hooks.
React.memo(Component)or awithAuth(Component)wrapper adds behavior to a component without changing it. - TypeScript and Angular decorators.
@Injectable(),@Input()and NestJS@UseGuards()attach behavior to classes and methods. The syntax differs from the classic pattern, but the idea of wrapping to add behavior is the same. - Node.js streams.
readable.pipe(gzip).pipe(encrypt)layers behaviors over a stream.
Decorator, Proxy and inheritance
| Purpose | Who decides to wrap | |
|---|---|---|
| Decorator | Add behavior | The client, at composition time, freely stacked |
| Proxy | Control access to the object | Usually fixed, one layer standing in for the real thing |
| Inheritance | Extend a class | Fixed at compile time, one combination per subclass |
Prefer composition when behaviors combine. Inheritance is fine when there is one clear "is a" relationship and no combinations.
When not to use it
- Order matters and is easy to get wrong. Auth wrapped inside logging behaves differently from logging inside auth. If the order is fragile, document it or build the chain in one place.
- Debugging depth. A stack trace through six wrappers is hard to read. Keep chains short and name the layers.
- The interface is huge. Every decorator has to forward every method. For a wide interface that is a lot of boilerplate, and a Proxy or a language feature may serve better.
- A simple flag is enough. If you only ever need one optional behavior, an option is lighter than a class.
- Identity checks.
instanceof Coffeefails on a decorated coffee, because the outer object is a decorator.
What interviewers ask
- "What is the Decorator pattern?" Wrapping an object with another that shares its interface, to add behavior dynamically.
- "Why not inheritance?" Inheritance needs a subclass per combination, and is fixed at compile time.
- "Decorator versus Proxy?" Decorator adds behavior and is stacked by the client. Proxy controls access, usually one layer.
- "Give a real example." Express middleware,
React.memo, or a caching wrapper. - "Is a TypeScript decorator the same thing?" Related in spirit, but it is a language feature that modifies a class or method at definition time, not the classic runtime wrapper.
The mistake to avoid
A Decorator chain that assumes a fixed wrap order. Reading this.coffee.cost() before adding its own charge is what makes the order not matter. Skip that, and stacking two decorators gives a different total depending on which wraps which. Each decorator should depend only on the interface of what it wraps, never on what is wrapped inside that.
The mechanism above, an object holding a reference to another of the same interface, is the same in JavaScript, Python or Java. Only the syntax changes.
Now practice it
1 challenge · 2 quizzesReading this page is the cheap half. These are the exercises that make you use it.
- DecoratorProStack behavior onto an object at runtime without touching its class — the same wrap-and-forward mechanic this page walks through.ChallengeSolve · MediumChallengeSolve · Medium
- Design Patterns · Structural PatternsTen quick questions across Adapter, Decorator, Facade, Proxy, Bridge, Composite and Flyweight, to check the mental model before this one.QuizTake · MediumQuizTake · Medium
- Design Patterns · Structural Patterns — Series 3ProA second, tougher pass on the same structural family, for once the first set feels easy.QuizTake · MediumQuizTake · Medium
Read next
Adapter pattern
Translating an interface you cannot change into the one callers expect, the extra hop every call pays, and the moment it adds behavior and becomes a Decorator.
5 min
Proxy pattern
Controlling access to, or deferring the cost of, an object behind its own interface, the extra hop easy to forget, and the moment it carries logic and becomes a Decorator.
7 min