Adding behavior to an object at runtime without touching its class — the Coffee/Milk/Sugar example, and why it avoids a combinatorial explosion of subclasses.
Published September 22, 2026
Say you're modeling a coffee shop's menu: a base coffee, optionally with milk, optionally with sugar, optionally with whip. If you model each combination as its own subclass, you get a combinatorial explosion — Coffee, CoffeeWithMilk, CoffeeWithSugar, CoffeeWithMilkAndSugar, CoffeeWithMilkAndSugarAndWhip... every new optional add-on doubles the number of subclasses needed to cover every combination.
interface Beverage {
double cost();
String description();
}
class Coffee implements Beverage {
public double cost() { return 2.00; }
public String description() { return "Coffee"; }
}
// Decorators wrap a Beverage and ADD behavior, implementing the same interface
abstract class BeverageDecorator implements Beverage {
protected final Beverage wrapped;
BeverageDecorator(Beverage wrapped) { this.wrapped = wrapped; }
}
class Milk extends BeverageDecorator {
Milk(Beverage wrapped) { super(wrapped); }
public double cost() { return wrapped.cost() + 0.50; }
public String description() { return wrapped.description() + " + Milk"; }
}
class Sugar extends BeverageDecorator {
Sugar(Beverage wrapped) { super(wrapped); }
public double cost() { return wrapped.cost() + 0.25; }
public String description() { return wrapped.description() + " + Sugar"; }
}
class Whip extends BeverageDecorator {
Whip(Beverage wrapped) { super(wrapped); }
public double cost() { return wrapped.cost() + 0.75; }
public String description() { return wrapped.description() + " + Whip"; }
}
Beverage order = new Whip(new Sugar(new Milk(new Coffee())));
System.out.println(order.description()); // "Coffee + Milk + Sugar + Whip"
System.out.println(order.cost()); // 2.00 + 0.50 + 0.25 + 0.75 = 3.50
Each decorator calls wrapped.cost()/wrapped.description() first, then adds its own contribution — a chain of delegating calls, each adding one increment. Any combination of add-ons is just a different order of wrapping, chosen at runtime, instead of a separate class that had to be written in advance for that exact combination.
With N optional add-ons, subclassing needs up to 2^N classes to cover every combination. Decorator needs exactly N decorator classes (one per add-on) — any combination is composed at runtime by nesting, not pre-declared at compile time. Adding a new add-on (say, Caramel) means writing exactly one new decorator class; it doesn't require touching or regenerating any existing combination classes.
Inheritance fixes behavior additions at compile time — a CoffeeWithMilk class is permanently "coffee with milk," full stop. Decorator adds behavior at runtime, through composition — the exact same Coffee instance can be wrapped differently in different code paths, or have its wrapping decided by user input ("add milk? add sugar?") rather than baked into a class hierarchy chosen ahead of time. This is the same underlying principle as "favor composition over inheritance" from the OOP refresher — Decorator is one of the concrete patterns that principle produces when applied to "add behavior dynamically."
Q: Is Java's BufferedReader(new FileReader(...)) an example of Decorator?
A: Yes — this is the textbook real-world instance in the JDK itself: Reader/InputStream/OutputStream/Writer are all decorator-friendly interfaces, and wrapping a FileReader in a BufferedReader adds buffering behavior without FileReader itself needing to know or change.
Q: How is Decorator different from Proxy, since both wrap an object behind the same interface? A: Decorator's purpose is adding behavior (the wrapped object's own behavior still runs, plus more); Proxy's purpose is controlling access to the wrapped object (the access might be restricted, deferred/lazy, or redirected — the wrapped object's behavior might not run at all, e.g. an access-denied proxy). Structurally similar, semantically different intents (see Composite & Proxy).
Q: What's a real downside of Decorator? A: Deeply nested decorator chains can be hard to debug — a stack trace through five layers of wrapping, each delegating to the next, is harder to read than a single class, and the order of wrapping can matter in non-obvious ways (e.g. a logging decorator wrapped inside vs outside a caching decorator behaves very differently).
Q: Could you implement the Coffee example with a List<Addon> field instead of nested wrapper objects?
A: You could, and for simple additive cost/description logic it's arguably simpler — but you'd lose Decorator's core property of each add-on implementing the full Beverage interface itself (so a decorated beverage can be passed anywhere a plain Beverage is expected, transparently) and its ability to override/intercept behavior more elaborately than a flat list of additive modifiers could.