intermediate~2h

Decorator Pattern

Learn how the Decorator pattern adds optional, combinable behavior to an individual object at runtime by wrapping it, avoiding the combinatorial subclass explosion that inheritance-based designs run into.

Learning objectives

  • Explain why subclassing every combination of optional features doesn't scale
  • Build an abstract decorator that wraps an interface and stacks with other decorators
  • Trace how stacked decorators compose their behavior at runtime
  • Tell Decorator and Proxy apart by intent even though their structure looks similar

◆ Story

Ordering a coffee with milk, an extra shot, and caramel doesn't require the shop to keep a pre-made class of drink for every possible combination of add-ons. They start with a plain coffee and layer each addition on top, one at a time, each layer adding its own cost and its own line to the description of whatever came before it.

This chapter builds that same layering idea around a pizza ordering system: start from a plain BasicPizza, and let a customer add cheese, olives, or mushrooms in any combination and any order, without the existing pizza classes needing to change every time a new combination comes up.

◆ The problem

The obvious way to model "pizza plus toppings" with plain object-oriented inheritance is a subclass per combination: CheesePizza extends BasicPizza, then CheeseOlivePizza extends CheesePizza for cheese and olives together, and so on. This actually works for the combinations someone bothered to write a class for — a CheeseOlivePizza correctly reports $6.50 as its cost.

The trouble is what happens as toppings multiply. The number of possible combinations grows combinatorially: with just four toppings, every subset is a potential pizza, up to 2⁴ = 16 combinations, each theoretically needing its own subclass. A customer who wants cheese and mushroom but not olives, and nobody happened to write that specific subclass in advance, simply has no class to represent their order. And testing this is genuinely painful — ten toppings means up to 2¹⁰ possible combinations to verify, which stops being remotely practical at that scale.

This is inheritance being used for something it's fundamentally bad at: composing several independent, optional behaviors together. Favoring composition over inheritance is the standard answer to exactly this shape of problem.

💻 Code example

interface Pizza { String getDescription(); double getCost(); } class BasicPizza implements Pizza { public String getDescription() { return "Basic pizza"; } public double getCost() { return 5.0; } } class CheesePizza extends BasicPizza { public String getDescription() { return super.getDescription() + " + cheese"; } public double getCost() { return super.getCost() + 1.0; } } class CheeseOlivePizza extends CheesePizza { // a new subclass, only for this specific combination public String getDescription() { return super.getDescription() + " + olives"; } public double getCost() { return super.getCost() + 0.5; } } // cheese + mushroom (no olives)? another subclass. cheese + olive + mushroom? another one again.

Each topping really only needs to know two things: what it adds to the description, and what it adds to the cost — nothing about which other toppings might also be present. Adding a topping to a pizza should produce something that's still a Pizza, so another topping can be layered on top of that result, and the process can repeat indefinitely.

That leads directly to the Decorator pattern: a "topping" is a wrapper — something that holds a Pizza, adds its own contribution on top, and is itself a Pizza, so it can be wrapped again by the next topping. An abstract PizzaDecorator implements Pizza and holds a decoratedPizza field for whatever it's wrapping. Concrete decorators like CheeseDecorator extend it: their constructor just forwards the wrapped pizza up to the parent, and their getDescription()/getCost() methods first ask the wrapped pizza for its own value, then add their own contribution on top. CheeseDecorator knows nothing about OliveDecorator's existence — they're independent wrappers that can be layered onto any Pizza, including onto each other, in any order.

💻 Code example

abstract class PizzaDecorator implements Pizza { protected Pizza decoratedPizza; // holds ANOTHER Pizza -- the "wrap" half of the dual role public PizzaDecorator(Pizza pizza) { this.decoratedPizza = pizza; } } class CheeseDecorator extends PizzaDecorator { public CheeseDecorator(Pizza pizza) { super(pizza); } public String getDescription() { return decoratedPizza.getDescription() + " + cheese"; // ask the wrapped pizza first, then add } public double getCost() { return decoratedPizza.getCost() + 1.0; } } class OliveDecorator extends PizzaDecorator { public OliveDecorator(Pizza pizza) { super(pizza); } public String getDescription() { return decoratedPizza.getDescription() + " + olives"; } public double getCost() { return decoratedPizza.getCost() + 0.5; } } class MushroomDecorator extends PizzaDecorator { public MushroomDecorator(Pizza pizza) { super(pizza); } public String getDescription() { return decoratedPizza.getDescription() + " + mushroom"; } public double getCost() { return decoratedPizza.getCost() + 2.0; } }

Building an order now means starting from a BasicPizza and wrapping it, one decorator at a time, reassigning the same pizza variable at each step. Every wrap still satisfies the Pizza interface, so the next decorator can wrap whatever came before it without caring whether that was a plain pizza or an already-decorated one.

Adding a third topping — cheese, olives, and mushroom together — needs no new class at all. Compare this to the inheritance version: that combination would have required its own dedicated subclass, written in advance, by someone who anticipated exactly that combination. Here it's three lines of composition, using three classes that were never written with "cheese + olive + mushroom" specifically in mind. n topping classes now cover every possible combination of those toppings, instead of needing a separate class per combination.

💻 Code example

public class Main { public static void main(String[] args) { Pizza pizza = new BasicPizza(); pizza = new CheeseDecorator(pizza); pizza = new OliveDecorator(pizza); pizza = new MushroomDecorator(pizza); // stack a third layer, zero new pizza classes System.out.println(pizza.getDescription()); // "Basic pizza + cheese + olives + mushroom" System.out.println(pizza.getCost()); // 8.5 } }

▲ Edge case — order can matter, even when the math doesn't

In this example, getCost() is pure addition, so the order toppings are wrapped in never changes the final total. But if a decorator applied a percentage discount instead of a flat addition, stacking order would suddenly matter a great deal -- a 10%-off decorator applied before versus after a flat surcharge decorator produces different final prices. Whenever decorators aren't simply additive, the order they're applied in becomes a real design decision, not an implementation detail.

▲ Edge case — this is a live example of two design principles, not just a way to avoid subclass explosion

Each decorator has exactly one job -- CheeseDecorator only ever knows about cheese -- which is a direct Single Responsibility win. And BasicPizza is never modified to support a new topping; only new decorator classes get added, which is a direct Open/Closed win. Decorator is a genuinely concrete example of both principles operating together, not just an abstract pattern name.

▲ Edge case — a stack of decorators still calls through every layer

Every method call on a heavily-decorated object passes through every wrapper in the stack before reaching the innermost real object. For a handful of layers this overhead is negligible, but a very deep stack of decorators does add real, if usually small, per-call cost.

◆ Where this shows up

Java's own I/O classes are a genuinely famous real use of this exact pattern: new BufferedInputStream(new FileInputStream("f.txt")) wraps a decorator (buffering) around a base component (the file stream), and both implement InputStream. Different combinations of stream wrappers -- buffering, compression, encryption -- can be stacked depending on what behavior is actually needed, exactly the way toppings stack in this chapter's example.

UI frameworks that let a component be wrapped with scrollbars, borders, or shadows independently of one another follow the identical shape: each visual add-on is its own wrapper implementing the same component interface as whatever it wraps. Anywhere behavior needs to be added to individual objects at runtime, combinably and without touching the wrapped class, Decorator is the pattern doing the work.

Q: What problem does using inheritance for optional, combinable features create? A: A combinatorial subclass explosion -- every possible combination of features needs its own dedicated subclass, growing roughly as 2^n for n optional features.

Q: What dual role does each concrete decorator play, and why does it matter? A: It implements the shared interface (so it can stand in anywhere the interface is expected) and holds a reference to an instance of that same interface (so it can wrap and delegate to whatever came before it). That dual role is what lets decorators stack in any order at runtime.

Q: Why is PizzaDecorator declared abstract instead of providing default cost/description logic? A: Because it has no way of knowing what any specific topping should add -- that knowledge genuinely differs per concrete decorator, so it's left to each subclass to implement.

Q: How does Decorator satisfy both SRP and OCP at the same time? A: SRP, because each decorator has exactly one topping's worth of responsibility. OCP, because BasicPizza is never modified to support a new topping -- a new decorator class is added instead.

Q: When does the order decorators are stacked in actually matter? A: When their effects aren't simply additive -- a percentage-based decorator produces a different final result depending on whether it's applied before or after a flat-amount decorator.

Want a visual for this concept?

Generate a diagram tailored to “Decorator Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Proxy Pattern← Back to all Low-Level Design & Design Patterns chapters