Beginner12 min readDesign Patternslive prototype

Decorator

Add behavior to an object by placing it inside wrapper objects that share its interface. A plain `Espresso` knows its own `cost()`; wrap it — `new Whip(new Milk(new Espresso()))` — and each layer adds its own price, then passes the call along. Stack any combination at runtime, with no `EspressoWithMilkAndWhip` subclass zoo.

The idea

What it is

Decorator attaches new behavior to an object by placing it inside wrapper objects that share its interface — that's the whole intent in one sentence. Think of a coffee order. An Espresso is a Coffee: it has a cost() and a description(). Now the customer wants milk. You don't edit Espresso, and you don't create a MilkEspresso subclass. You wrap it: new Milk(new Espresso()). The Milk wrapper is also a Coffee — same interface — so anyone holding it can call cost() exactly as before. Inside, Milk asks the espresso for its cost and adds $0.50 on top.

The trick that makes it powerful: because every wrapper looks like a plain Coffee, wrappers can wrap other wrappers. Want milk, mocha, and whip? new Whip(new Mocha(new Milk(new Espresso()))). Each layer adds one small thing and delegates the rest. You compose behavior at runtime, layer by layer, in any order and any combination — something plain inheritance simply can't do, because a class's parents are fixed forever at compile time.

The one sentence to remember

A decorator wraps an object, implements the same interface, and adds its own behavior before or after delegating to the object it wraps. To the outside world, the wrapped stack is indistinguishable from the plain object.

Not the same as Python's @decorator syntax

Python's @decorator functions share the name and the spirit (wrap a thing, keep its shape, add behavior) but they wrap functions, not objects, and they're applied once at definition time. The GoF Decorator pattern wraps objects at runtime. Related idea, different mechanism.

Mechanics

How it works

The four participants

Every textbook Decorator has the same four roles. In coffee terms:

  • Component interfaceCoffee, with cost() and description(). This is the shape everyone agrees on; both real coffees and wrappers implement it.
  • Concrete ComponentEspresso, the plain object at the core of every stack. It answers cost() with its own price and doesn't know wrappers exist.
  • Base Decorator — a small abstract class that holds a reference to a wrapped `Coffee` and, by default, just forwards every call to it. Its only job is the plumbing.
  • Concrete DecoratorsMilk, Mocha, Whip, Caramel. Each extends the base decorator and overrides methods to add its own bit: its price, its name — then delegates inward.
typescript
interface Coffee {
  cost(): number;
  description(): string;
}

class Espresso implements Coffee {          // the core
  cost() { return 2.5; }
  description() { return "Espresso"; }
}

abstract class CoffeeDecorator implements Coffee {
  constructor(protected wrapped: Coffee) {} // 🔑 holds what it wraps
  cost() { return this.wrapped.cost(); }    // default: just delegate
  description() { return this.wrapped.description(); }
}

class Milk extends CoffeeDecorator {
  cost() { return this.wrapped.cost() + 0.5; }        // add, then delegate
  description() { return this.wrapped.description() + " + Milk"; }
}

class Whip extends CoffeeDecorator {
  cost() { return this.wrapped.cost() + 0.7; }
  description() { return this.wrapped.description() + " + Whip"; }
}

const order: Coffee = new Whip(new Milk(new Espresso()));
order.cost();          // 3.7  — 2.5 + 0.5 + 0.7
order.description();   // "Espresso + Milk + Whip"

How one cost() call unwinds through the layers

Call cost() on new Whip(new Milk(new Espresso())) and the call travels inward: Whip.cost() asks Milk, Milk.cost() asks Espresso. Then the answers come back out: Espresso returns $2.50, Milk adds $0.50 and returns $3.00, Whip adds $0.70 and returns $3.70. Each layer does one small job on the way back. The caller never sees any of this — it called cost() on a Coffee and got a number.

Why not just subclass? Do the math

With 4 optional add-ons, covering every combination with subclasses means EspressoWithMilk, EspressoWithMilkAndMocha, EspressoWithMilkAndWhip… — one class per subset, which is 2⁴ = 16 classes. Add a fifth add-on and it's 32. With Decorator you write 4 small wrapper classes, ever, and compose any of the 16 combinations at runtime by stacking. That's the subclass explosion the pattern kills — and it's [[open-closed]] in action: you extend behavior by adding new wrapper classes, never by modifying Espresso.

Decorators wrap decorators — order can matter

Because a wrapper is a Coffee, it happily wraps another wrapper — that's what makes stacking work. Note the stack has an order: for prices it doesn't matter ($ adds up either way), but a decorator that, say, applies a 10% discount gives different totals depending on whether it wraps the milk or the milk wraps it.

You've already used this pattern

  • Java I/O streamsnew BufferedReader(new FileReader("log.txt")): FileReader is the core component, BufferedReader is a decorator that adds buffering while staying a Reader. GZIPInputStream, DataInputStream — same idea, stackable.
  • Web middleware — logging, auth, and compression layers each wrap a handler, do their bit, and delegate to the next. A request unwinds through them exactly like cost() unwinds through the coffee stack.
  • GUI components — the Gang of Four's original example: a ScrollDecorator or BorderDecorator wraps any visual component and adds scrollbars or a border without the component knowing.

Don't confuse it with two neighbors: [[adapter]] also wraps one object, but its goal is to change the interface so incompatible code can talk; Decorator keeps the interface and adds behavior. And [[composite]] aggregates many children behind one interface, while a Decorator wraps exactly one object and enhances it.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A coffee-order builder. The ☕ Espresso ($2.50) sits at the core. Click an add-on chip — 🥛 Milk, 🍫 Mocha, 🍦 Whip, 🍯 Caramel — and a labeled ring wraps around the whole order; the newest wrapper is always the outermost ring. The mono readout shows the real composed expression, e.g. new Whip(new Milk(new Espresso())), plus the current cost(). Press ▶ cost() and watch a pulse travel from the outermost ring inward to the espresso, then back out — each ring flashes its +$x on the way out while the total counts up. That's the whole pattern in one animation: the call passes inward, each wrapper adds its bit on the return. The tiny counter reminds you what you're not writing: 16 subclasses for 4 add-ons.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Build an order by wrapping

Open the prototype. The core ring is ☕ Espresso at $2.50. Click the 🥛 Milk +$0.50 chip — a Milk ring wraps around the espresso, and the readout updates to new Milk(new Espresso()). Add 🍦 Whip +$0.70 and watch it become the new outermost ring: new Whip(new Milk(new Espresso())). Newest wrapper is always on the outside — that's the nesting order of the constructor calls.

try 02

Run cost() and watch it unwind

Press ▶ cost(). A pulse travels from the outermost ring inward to the espresso — that's the call delegating layer by layer. Then it comes back out: each ring flashes its +$x as the total counts up ($2.50 → $3.00 → $3.70). One animation, whole pattern: call goes in, prices add up on the way out.

try 03

Toggle combinations, count the classes you didn't write

Click a chip that's already on to unwrap that layer, then stack a different combination — Mocha + Caramel, all four, whatever you like. Every combination works because each wrapper only knows the Coffee it wraps. The little counter chip says it plainly: covering these combos with inheritance would take 16 subclasses; here it's 4 decorators. Press ↺ Reset to get back to a plain espresso.

In practice

When to use it — and what trips people up

Reach for Decorator when behavior comes in optional, stackable pieces

  • Add responsibilities at runtime — the caller decides while the program runs which extras an object gets (this order wants milk and whip; the next one doesn't). Inheritance fixes behavior at compile time; wrapping composes it on the fly.
  • Combinations of optional features — buffering + encryption + compression on a stream; logging + auth + rate-limiting on a handler. A handful of decorators covers every subset; subclasses would explode (n features → 2ⁿ classes).
  • When subclassing is closed to you — the class is final, comes from a third-party library, or you only ever hold it behind an interface. You can't extend it, but you can always wrap it.
  • Keeping the core class smallEspresso stays a dumb, honest espresso. Every extra lives in its own tiny class with one job. That's [[open-closed]]: open to extension by new wrappers, closed to modification.

When NOT to use it

If there's exactly one fixed extra behavior and it never varies, a plain subclass — or just a field/flag on the class — is simpler and easier to read. Decorator pays off when extras are optional and combinable. And pick the right wrapper: [[adapter]] wraps to change the interface, Decorator wraps to keep it and add behavior; [[composite]] holds many children, Decorator wraps exactly one.

What it gives you

  • Add or remove responsibilities at runtime by wrapping and unwrapping — no recompiling, no editing the core class.
  • Kills the subclass explosion: n optional features need n small wrapper classes instead of 2ⁿ subclasses for every combination.
  • Single Responsibility in practice — each decorator is a tiny class that does exactly one thing (add milk, add buffering, add logging).
  • Open-Closed in practice — new behavior means a new wrapper class; existing components and decorators never change.

Common mistakes

  • Many small objects — a heavily decorated system is a forest of little wrappers that look alike, which can make debugging and stack traces harder to follow.
  • Identity breaks: the decorated object is not the same object as the core (order !== espresso), so identity checks and some type checks (instanceof Espresso) fail on the wrapped stack.
  • Order can matter — a discount decorator gives different results wrapping the milk vs. being wrapped by it, and nothing in the types warns you.
  • Hard to remove a layer from the middle of the stack — wrappers only know what's inside them, so pulling one out means rebuilding the chain around it.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

interface Coffee {
  cost(): number;
  description(): string;
}

// Concrete component — the plain object at the core.
class Espresso implements Coffee {
  cost(): number { return 2.5; }
  description(): string { return "Espresso"; }
}

// Base decorator — holds a wrapped Coffee, delegates by default.
abstract class CoffeeDecorator implements Coffee {
  constructor(protected wrapped: Coffee) {}
  cost(): number { return this.wrapped.cost(); }
  description(): string { return this.wrapped.description(); }
}

// Concrete decorators — each adds one small thing, then delegates.
class Milk extends CoffeeDecorator {
  cost(): number { return this.wrapped.cost() + 0.5; }
  description(): string { return this.wrapped.description() + " + Milk"; }
}

class Mocha extends CoffeeDecorator {
  cost(): number { return this.wrapped.cost() + 0.8; }
  description(): string { return this.wrapped.description() + " + Mocha"; }
}

class Whip extends CoffeeDecorator {
  cost(): number { return this.wrapped.cost() + 0.7; }
  description(): string { return this.wrapped.description() + " + Whip"; }
}

// Stack any combination at runtime — no subclass zoo.
const order: Coffee = new Whip(new Mocha(new Milk(new Espresso())));
console.log(order.description()); // Espresso + Milk + Mocha + Whip
console.log(order.cost());        // 4.5

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is the intent of the Decorator pattern?

question 02 / 05

Why can Milk wrap Mocha, which wraps Espresso — and the caller never notices?

question 03 / 05

A café has 4 optional add-ons. Covering every combination with plain subclasses vs. decorators takes how many classes?

question 04 / 05

You call cost() on new Whip(new Milk(new Espresso())). What actually happens?

question 05 / 05

Which of these is a real drawback of Decorator?

0/5 answered