codejump

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

Waiting at the end of this page.

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

Each decorator implements the same cost() interface, adds its own bit, and forwards the rest to whatever it wraps
Each decorator implements the same cost() interface, adds its own bit, and forwards the rest to whatever it wraps

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

Each wrapper adds one concern around the original handler, and any subset can be applied to any handler
Each wrapper adds one concern around the original handler, and any subset can be applied to any handler
Use caseWhat the decorator adds
HTTP middlewareLogging, authentication, CORS, rate limiting around a handler.
CachingA cache layer in front of a function or repository.
Retries and timeoutsWrapping a network call with retry logic.
I/O streamsBuffering, compression or encryption around a stream.
UI componentsA border, a scrollbar or a tooltip around a widget.
PermissionsA check wrapped around an action.
Metrics and tracingTiming and counters around any operation.
Feature flagsEnabling 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) or memoize(fn) returns a new function with the same signature and extra behavior.
  • React higher-order components and hooks. React.memo(Component) or a withAuth(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

PurposeWho decides to wrap
DecoratorAdd behaviorThe client, at composition time, freely stacked
ProxyControl access to the objectUsually fixed, one layer standing in for the real thing
InheritanceExtend a classFixed 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 Coffee fails 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 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.