I reach for this pattern the moment someone says "and it should also optionally do X, Y, and Z, in any combination." That phrase is the tell. By the end of this page you should be able to recognize when Decorator fits, draw it, and write it under interview pressure without stalling.
If you remember five things
The whole pattern in one glance. Everything below defends these five calls.
- Intent. Add responsibilities to one object at runtime by wrapping it, not by subclassing the whole class.
- The tell. "Optional features that combine in any subset, any order" means Decorator. Subclassing every combination blows up combinatorially.
- The dual relationship. A decorator is-a Component (clients treat it identically) and has-a Component (so it can delegate inward). Both halves, or it is not Decorator.
- Default-forward. The abstract decorator forwards every method by default, so concrete decorators only override what they change and stay tiny.
- The classic gotcha. A concrete decorator that returns its own slice instead of adding to inner.cost() silently drops every inner layer. Always add to what you get back.
1The problem it solves#
Start with the pain, because the pattern only makes sense once the naive versions hurt.
The setup. A coffee shop ordering system with base drinks and optional add-ons.
- Base drinks:
Espresso,HouseBlend,DarkRoast. - Add-ons the PM wants: milk, soy, whipped cream, caramel, an extra shot.
- Each add-on changes both the price and the description.
Naive move one: subclass every combination.
EspressoWithMilk,EspressoWithMilkAndCaramel,DarkRoastWithSoyAndWhipAndExtraShot, and on and on.- The subset math with 5 drinks and 6 add-ons is 5 times 2 to the 6th, which is 320 classes.
- Every new topping doubles that pile. Nobody maintains 320 classes.
Naive move two: booleans in the base class.
- Fields like
hasMilk,hasCaramel,hasWhipon the drink itself. cost()becomes a tangle ofifchecks, and the base class now knows about every topping that will ever exist.- You edit the base class for every new syrup, so you break Open/Closed every single time.
- That
cost()method also becomes a merge-conflict magnet: every team adding a topping edits the same place.
What we actually want.
- Start with a plain drink, wrap it with milk, wrap that with caramel.
- Each layer adds its own slice of price and description.
- Behavior stacks. That stacking is Decorator.
Say the combinatorial explosion out loud first ("320 classes, doubling with each topping"), then "so instead of subclassing every combination, I wrap." Naming the pain before the pattern is what signals you chose Decorator, rather than pattern-matched a keyword.
2The idea (mental model)#
The picture I draw at the whiteboard: Russian nesting dolls, but every doll speaks the same language.
- The innermost object is the real thing, a plain espresso.
- Each wrapper implements the same interface as the thing it wraps and holds a reference to it.
- On any call, a wrapper does two things: adds its own bit, then delegates the rest to the object inside it.
Trace cost() on a Caramel(Milk(Espresso)).
- Caramel adds 0.50 and asks Milk for its cost.
- Milk adds 0.40 and asks Espresso.
- Espresso returns its base price, 2.50.
- Values sum on the way back out: 2.50, then 2.90, then 3.40.
- The caller never knows how many layers are in there. It just sees "something with a
cost()."
The constraint that is the whole trick.
- A decorator is-a component, so clients treat it identically to the real thing.
- A decorator has-a component, so it can delegate inward.
- Only is-a: you are doing plain inheritance.
- Only has-a: you cannot chain transparently.
Reveal the answer
3Structure#
Four roles. Keep them straight and the diagram draws itself.
- Component: the shared interface. Both real objects and wrappers implement it. Here it is
Beverage. - ConcreteComponent: the real object you start with.
Espresso,DarkRoast. - Decorator: an abstract wrapper that holds a Component reference and implements Component. It forwards calls by default, so concrete decorators only override what they change.
- ConcreteDecorator: a specific add-on.
Milk,Caramel,WhipCream. Each adds behavior around the delegated call.
The relationship that matters:
- The abstract Decorator inherits from Component (is-a) and holds a Component (has-a).
- I draw the has-a as aggregation, not composition: the wrapped object has its own life and gets passed in from outside, rather than being created and owned by the wrapper.
AddonDecorator is-a Beverage and also holds a Beverage (the o-- aggregation edge). That self-referential shape is exactly what lets you wrap a decorator inside another decorator for as many layers as you like.4Implementation in Java#
Keep it tight: the interface, one concrete drink, the abstract decorator, two add-ons. That is enough to show the whole design.
public interface Beverage {
double cost();
String description();
}
public class Espresso implements Beverage {
public double cost() { return 2.50; }
public String description() { return "Espresso"; }
}
// Abstract decorator: is-a Beverage, has-a Beverage.
public abstract class AddonDecorator implements Beverage {
protected final Beverage inner;
protected AddonDecorator(Beverage inner) {
this.inner = inner;
}
// Sensible defaults: forward everything. Subclasses override what they change.
public double cost() { return inner.cost(); }
public String description() { return inner.description(); }
}
public class Milk extends AddonDecorator {
public Milk(Beverage inner) { super(inner); }
@Override
public double cost() { return inner.cost() + 0.40; }
@Override
public String description() { return inner.description() + ", Milk"; }
}
public class Caramel extends AddonDecorator {
public Caramel(Beverage inner) { super(inner); }
@Override
public double cost() { return inner.cost() + 0.50; }
@Override
public String description() { return inner.description() + ", Caramel"; }
}
Two things worth calling out while you write this:
innerisfinal. A wrapped object cannot be swapped mid-flight, so the chain stays immutable and safe to share.- The abstract class forwards both methods. A decorator that only touches price does not write boilerplate for
description(). That default-forward is what keeps concrete decorators tiny.
Using it reads like plain English, built from the inside out:
Beverage order = new Caramel(new Milk(new Espresso()));
System.out.println(order.description()); // Espresso, Milk, Caramel
System.out.println(order.cost()); // 3.40
- The client holds a
Beverage. It has no idea whether that is a bare espresso or a three-layer wrap. - That is exactly the point: add a
WhipCreamdecorator tomorrow and touch zero existing classes. - Open for extension, closed for modification, for real this time.
Here is the call falling inward and the values summing on the way back out:
inner.cost() call (the classic bug in section 7) silently erases every layer below the break.5A real-world example#
You do not have to invent this pattern. It is already in the standard library and your web stack.
java.io is the textbook case.
InputStreamis the Component.FileInputStreamis the ConcreteComponent, the thing that actually reads bytes off disk.new BufferedInputStream(new FileInputStream(...))adds buffering by wrapping.- Wrap that again with
GZIPInputStreamto decompress. - Every wrapper is still an
InputStream, so downstream code neither knows nor cares how many layers it reads through. That is why the constructors nest the way they do.
It shows up all over web backends.
- HTTP middleware is Decorator in spirit: a handler wrapped by logging, wrapped by auth, wrapped by a rate limiter. Each layer does its bit and calls the next.
- Java's
Collections.unmodifiableListandsynchronizedListare decorators too: each returns aListthat wraps yourListand changes one aspect, read-only in one case, thread-safe in the other.
In LLD interviews specifically, this is the clean answer to the coffee shop problem, the pizza-with-toppings problem, and any "notification that can go over email, then also SMS, then also Slack" problem.
6When to use it, and when not to#
Use it when:
- You need to add responsibilities to individual objects at runtime, not to a whole class.
- The responsibilities are optional and combine freely, any subset, any order.
- Subclassing would blow up combinatorially (recall the 320 classes), or the base class would have to know about every option.
Skip it when:
- There is a fixed, small set of variants that never mix. Plain subclasses or an enum are simpler, and simpler wins.
- Wrapping order changes correctness in ways that confuse people. If
Encrypt(Compress(x))andCompress(Encrypt(x))differ and callers get it wrong, that is a trap you built. (Compressing already-encrypted data barely shrinks it, so the order is not cosmetic.) - You need to reach into the chain and ask "is there a caramel somewhere in here?" Decorator hides the layers on purpose, so type-checking through the stack is awkward and usually a sign you want a different structure.
Know the lookalikes cold, because interviewers probe the boundaries:
- Proxy looks identical in code (wraps, same interface) but the intent differs: Proxy controls access (lazy loading, permissions, remoting) while keeping behavior the same. Decorator adds behavior you asked for.
- Adapter also wraps, but it changes the interface so two incompatible things fit. Decorator keeps the interface the same and changes behavior.
- Composite builds tree-shaped structures of a common interface too, but it is about part-whole hierarchies, not stacking behavior on one object.
7Common mistakes#
These are the ones that get you dinged. The second is the single most common failure in a live interview.
1. Changing the interface.
- A decorator that adds a public method the base interface lacks breaks transparency.
- Now the client must know it is holding a decorator to use that method, and the "treat them the same" benefit is gone.
2. Forgetting to delegate (the classic gotcha).
- The bug: your concrete decorator overrides
cost(), adds its 0.50, and returns that flat instead ofinner.cost() + 0.50. - Every inner layer vanishes from the total. Your 3.40 order silently rings up as 0.50.
- Always add to what you get back from
inner, never replace it.
Writing return 0.50; instead of return inner.cost() + 0.50; and not catching it when you trace the call. If I ask you to walk a cost() through the chain and your numbers do not sum, that tells me you can draw the pattern but have not actually run it. Trace one call out loud, 2.50 to 2.90 to 3.40, every time.
3. Putting state or identity logic in decorators.
- Keep decorators thin and stateless where possible. A decorator with mutable state, wrapped twice over the same object, produces surprises.
equals()andhashCode()across a wrapped chain get ugly fast: two logically equal orders built in different wrap orders will not compare equal.
4. Deep chains with no limit.
- Nothing stops you from wrapping 40 layers, and each call now bounces through 40 stack frames.
- Rarely a real problem, but worth one sentence in an interview to show you thought about it.
5. Order-dependence you did not document.
- If wrapping order matters, say so, and provide a builder that enforces a sane order rather than letting callers nest freely.
I am not testing whether you memorized four role names. I am testing whether you reach for the pattern when the problem calls for it, draw the is-a plus has-a loop without prompting, and trace a real call through the layers. A candidate who ties it back to Open/Closed in their own words has understood it, not memorized it. At a senior or staff bar, that is often the whole signal.
8Where it shows up in interviews#
When you see any of these prompts, Decorator is a strong answer:
- "Design a coffee shop, pizza, or sandwich builder where items have optional add-ons that affect price and description." The canonical prompt. Reach for Decorator immediately.
- "Design a notification system that can send over multiple channels, and new channels get added later." Wrap a base sender with channel decorators.
- "Add optional cross-cutting behavior (logging, caching, compression, retry) to a service without editing it." Decorator, or its cousin the middleware chain.
How to score the points, in order:
- Name the combinatorial-explosion problem out loud (the 320 classes), then say "so instead of subclassing, I wrap."
- Draw the is-a plus has-a relationship and call it out, because that dual relationship is what the interviewer is checking.
- Write the abstract decorator that forwards by default so concrete ones stay tiny.
- Build an instance from the inside out and trace one
cost()call, 2.50 to 2.90 to 3.40.
Do those four things and you have demonstrated the pattern cleanly.
Start plain, wrap to add, and let every layer speak the same language as the one below it.
"How is this different from Proxy? The code looks identical."
"Why not just use inheritance or booleans in the base class?"
"What is the most common bug people write in this pattern?"
"Does wrapping order ever matter? How do you protect callers?"
"Can I ask the chain whether it contains a specific decorator?"
What you can now explain in the room
- When the "optional features in any combination" tell means Decorator, and why subclassing explodes to 320 classes.
- The dual is-a plus has-a relationship, and why losing either half breaks the pattern.
- How to draw the four roles and the self-referential aggregation loop.
- Why the abstract decorator forwards by default, and why `inner` is final.
- The classic delegation bug (returning a flat value instead of adding to `inner`) and how to catch it by tracing one call.
- How Decorator differs from Proxy, Adapter, and Composite, and how it keeps the base class closed for modification.
If you liked this, look at the Composite pattern (also builds tree-shaped structures of a common interface) and Proxy (same wrapping shape, different intent), and try the notification-service and pizza-builder LLD problems to practice spotting when to wrap.
Get a prep plan built around your Decorator Pattern interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.