Home › Low-Level Design › Decorator Pattern
D

Decorator Pattern

Decorator lets you bolt extra behavior onto an object at runtime by wrapping it, without touching its class or exploding into a hundred subclasses.

Low-Level DesignMedium9 min read

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.

  1. Intent. Add responsibilities to one object at runtime by wrapping it, not by subclassing the whole class.
  2. The tell. "Optional features that combine in any subset, any order" means Decorator. Subclassing every combination blows up combinatorially.
  3. 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.
  4. Default-forward. The abstract decorator forwards every method by default, so concrete decorators only override what they change and stay tiny.
  5. 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, hasWhip on the drink itself.
  • cost() becomes a tangle of if checks, 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.
The sentence that earns the pattern

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.
Pause before you read on: if a decorator only held a reference to the component but did NOT implement the component interface, what breaks?
Reveal the answer
You lose transparency. Clients can no longer treat the wrapped object like the original, so you cannot pass a decorated drink anywhere a plain Beverage is expected, and you cannot wrap a decorator inside another decorator. The is-a half is what makes the chain composable and invisible to callers. Interviewers are specifically checking that you can name and draw both halves.

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.
classDiagram class Beverage { <> +cost() double +description() String } class Espresso { +cost() double +description() String } class DarkRoast { +cost() double +description() String } class AddonDecorator { <> #Beverage inner +cost() double +description() String } class Milk { +cost() double +description() String } class Caramel { +cost() double +description() String } class WhipCream { +cost() double +description() String } Beverage <|-- Espresso Beverage <|-- DarkRoast Beverage <|-- AddonDecorator AddonDecorator <|-- Milk AddonDecorator <|-- Caramel AddonDecorator <|-- WhipCream AddonDecorator o-- Beverage
Notice the loop: 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:

  • inner is final. 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 WhipCream decorator 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:

sequenceDiagram participant Client participant Caramel participant Milk participant Espresso Client->>Caramel: cost() Caramel->>Milk: inner.cost() Milk->>Espresso: inner.cost() Espresso-->>Milk: 2.50 Milk-->>Caramel: 2.90 Caramel-->>Client: 3.40
The request falls all the way inward to the real object before any number is produced, then each layer adds its slice on the way out. That is why dropping the 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.

  • InputStream is the Component.
  • FileInputStream is the ConcreteComponent, the thing that actually reads bytes off disk.
  • new BufferedInputStream(new FileInputStream(...)) adds buffering by wrapping.
  • Wrap that again with GZIPInputStream to 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.unmodifiableList and synchronizedList are decorators too: each returns a List that wraps your List and 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)) and Compress(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 of inner.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.
Red flag that fails you

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() and hashCode() 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.
From the interviewer's chair

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.

Follow-ups the interviewer will ask (and your answers)
"How is this different from Proxy? The code looks identical."
Same shape, different intent. Both wrap and implement the same interface. Decorator adds behavior you asked for (price, buffering, logging). Proxy controls access to the wrapped object (lazy loading, permission checks, remoting) while keeping its behavior the same. If I am adding features, it is Decorator. If I am gatekeeping, it is Proxy.
"Why not just use inheritance or booleans in the base class?"
Inheritance means a subclass per combination, which is 5 times 2 to the 6th, so 320 classes here, doubling with every new topping. Booleans put every option into the base class, so I edit that class for each new feature and break Open/Closed every time. Decorator keeps the base class closed and lets me add a new wrapper without touching anything existing.
"What is the most common bug people write in this pattern?"
Forgetting to delegate. A concrete decorator returns its own slice (return 0.50) instead of adding to the inner call (return inner.cost() + 0.50), which silently drops every layer below it. I catch it by tracing one cost() through the chain and checking the numbers sum: 2.50, 2.90, 3.40.
"Does wrapping order ever matter? How do you protect callers?"
Sometimes it changes correctness. Encrypt(Compress(x)) and Compress(Encrypt(x)) differ because compressing encrypted data barely shrinks it. When order is load-bearing, I document it and expose a builder that enforces a sane order rather than letting callers nest freely and get it wrong.
"Can I ask the chain whether it contains a specific decorator?"
You can, but you usually should not. Decorator hides the layers on purpose, so type-checking through the stack (is there a Caramel in here?) fights the pattern. If I genuinely need to inspect the composition, that is a signal the problem wants a different structure, maybe an explicit list of features rather than a wrap chain.

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.

Thanks. Check your inbox, your Decorator Pattern prep plan is on the way.