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 interface —
Coffee, withcost()anddescription(). This is the shape everyone agrees on; both real coffees and wrappers implement it. - Concrete Component —
Espresso, the plain object at the core of every stack. It answerscost()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 Decorators —
Milk,Mocha,Whip,Caramel. Each extends the base decorator and overrides methods to add its own bit: its price, its name — then delegates inward.
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 streams —
new BufferedReader(new FileReader("log.txt")):FileReaderis the core component,BufferedReaderis a decorator that adds buffering while staying aReader.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
ScrollDecoratororBorderDecoratorwraps 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.
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.
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.
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 small —
Espressostays 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.5References & further reading
5 sources- Articlerefactoring.guru
Decorator — Refactoring.Guru
The clearest illustrated walkthrough: the notification/stream problem, the wrapper structure, when to use it, and code in many languages. Best first read.
- Docsen.wikipedia.org
Decorator pattern — Wikipedia
Concise reference with UML, the Java I/O example, and notes on how the pattern relates to Adapter, Composite, and language-level decorators.
- Booken.wikipedia.org
Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides
The original Gang of Four book. Its Decorator chapter uses the classic GUI example — adding borders and scrollbars to visual components by wrapping.
- Booken.wikipedia.org
Head First Design Patterns — Freeman & Robson (Decorator chapter)
The famous 'Starbuzz Coffee' chapter is exactly this lesson's example — beverages and condiment decorators — told with the book's trademark visual style.
- Articlepython-patterns.guide
The Decorator Pattern in Python — python-patterns.guide (Brandon Rhodes)
A careful look at implementing the GoF pattern in Python, and how it differs from Python's built-in @decorator syntax for functions.
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