Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Decorator
LLD

Decorator

Understand how to dynamically add responsibilities or behavior to objects without modifying their classes.

Decorator: Wrapping With New Responsibilities

The Problem It Solves

A coffee ordering system needs espresso + milk + caramel + double-shot combinations. Subclass-per-combination explodes combinatorially (EspressoWithMilk, EspressoWithMilkAndCaramel, …). Cross-cutting needs show the same shape elsewhere: a repository that’s cached or audited or both, an HTTP client with retries and metrics and either order. Decorator solves it by wrapping objects in same-interface layers, each adding one behavior before/after delegating.

                    ┌────────────────────┐
   Coffee ◁─────────┤ «interface» Coffee │      cost() + name()
                    └─────────△──────────┘
                              │ implements (each layer)
              ┌───────────────┼────────────────┐
        Espresso       MilkDecorator    CaramelDecorator
        cost(): 200    cost(): wrap.cost()+30   cost(): wrap.cost()+50
                       ▲ each decorator HOLDS a Coffee and ADDS to it

 runtime stacking:
 new Caramel(new Milk(new Espresso()))  → 200+30+50 = 280
 new Milk(new Caramel(new Espresso()))  → same total, different layering semantics

The wrapper shares the wrapped object’s interface, so clients cannot tell how many layers exist — transparency is the point.

Mechanics

interface Coffee { int cost(); String name(); }

class Espresso implements Coffee {
    public int cost() { return 200; }
    public String name() { return "Espresso"; }
}

abstract class CoffeeDecorator implements Coffee {   // shared delegation plumbing
    protected final Coffee inner;
    CoffeeDecorator(Coffee inner) { this.inner = inner; }
}

class Milk extends CoffeeDecorator {
    Milk(Coffee c) { super(c); }
    public int cost() { return inner.cost() + 30; }          // add + delegate
    public String name() { return inner.name() + " + milk"; }
}

The JDK’s Most Famous Chain

new BufferedReader(
    new InputStreamReader(                                  // bytes → chars
        new FileInputStream(path), UTF_8),                  // file → bytes
    8192);                                                  // + buffering

All three implement Reader; each adds one responsibility (decoding, buffering) around the same core interface. This chain is the canonical citation for the pattern.

Production Uses

  • Collections.unmodifiableList(...), synchronizedList(...), checkedList(...) — behavior wrappers over any List.
  • Logging/timing/caching wrappers around repositories in layered backends.
  • Servlet HttpServletRequestWrapper for request mutation in filters.

Decorator vs Proxy vs Adapter vs Strategy

PatternSame interface?Purpose
DecoratorYesAdd responsibilities; stackable
ProxyYesControl access (lazy/authz/remote); usually not stacked
AdapterNo — convertsInterface translation
Strategyn/aReplace algorithm wholesale

Decorator vs proxy is the classic confusion: intent separates them (enrichment vs access control).

Trade-offs & Pitfalls

  • Gains: open-closed extension; runtime composition; combinatorial freedom without class explosion.
  • Costs: identity confusion — a decorated object fails ==/instanceof checks against the concrete type; code relying on concrete internals breaks.
  • Stack-order sensitivity: retry-outside-metrics vs metrics-outside-retry count different things; document intended layering.
  • Debugging: stack traces grow one frame per layer — deep stacks are normal here, not bugs.

Interview Framing

  • “Add caching AND logging to this service” answered by composing two decorators scores fully; subclassing CachedLoggingRepository loses the runtime-composition point.

My Private Notes

Notes are auto-saved locally to this device.