Intent
Attach additional responsibilities to an object dynamically by wrapping it in another object that shares the same interface. Decorator gives you a flexible alternative to subclassing for extending behavior.
That one-liner hides the whole trick: the wrapper is-a the thing it wraps (same interface), and it has-a the thing it wraps (holds a reference). Because of that dual nature, a decorator is transparent to clients — they cannot tell whether they hold a bare object or one swaddled in five layers of decorators.
The Problem
You have a DataSource that reads and writes bytes. Now product wants compression. Then encryption. Then maybe buffering. Then encryption on top of compression, but only for one feature, while another feature wants compression alone, and a third wants raw access.
The naive approach is inheritance:
DataSource
├─ CompressedDataSource
├─ EncryptedDataSource
├─ CompressedEncryptedDataSource
├─ EncryptedBufferedDataSource
└─ CompressedEncryptedBufferedDataSource ...
This is combinatorial class explosion. With n independent optional features you need on the order of 2ⁿ−1 additional subclasses to cover every non-empty combination (the all-off case is just the bare DataSource), and the choices are frozen at compile time — you cannot decide at runtime “this particular stream needs compression but not encryption.” Inheritance binds behavior statically and forces a single inheritance chain to encode every orthogonal concern.
What you actually want is to stack orthogonal behaviors at runtime, in any order, in any combination, without writing a class per combination. That is exactly what Decorator delivers: each feature becomes one wrapper class, and you get all 2ⁿ−1 non-trivial stackings (plus the base) by composing those n wrappers at runtime — no new classes per combination.
The decorator and the wrapped object implement the same interface. This is the non-negotiable rule. If your wrapper exposes a different or wider interface than what it wraps, you have a Proxy, an Adapter, or just an ad-hoc class — not a Decorator.
Structure
┌─────────────────────┐
│ <<interface>> │
│ Component │
│─────────────────────│
│ + operation(): T │
└──────────▲──────────┘
│ implements
┌──────────┴───────────────────────┐
│ │
┌───────────────────┐ ┌────────────────────────────┐
│ ConcreteComponent │ │ Decorator (abstract) │
│───────────────────│ │─────────────────────────────│
│ + operation(): T │ │ - wrappee: Component │◇──┐
└───────────────────┘ │─────────────────────────────│ │ has-a
│ + operation(): T │───┘ (same
│ { wrappee.operation() } │ interface)
└──────────────▲──────────────┘
│ extends
┌───────────────────┴────────────────────┐
┌──────────────────┐ ┌──────────────────────┐
│ ConcreteDecoratorA│ │ ConcreteDecoratorB │
│──────────────────│ │──────────────────────│
│ + operation() │ │ + operation() │
│ + addedBehavior()│ │ + addedState │
└──────────────────┘ └──────────────────────┘
The key edge is the diamond (◇): Decorator aggregates a Component, and because Decorator is-a Component, that wrappee can itself be another decorator. This recursion is what lets you build chains of arbitrary depth.
Participants
| Participant | Role |
|---|---|
| Component | The shared interface (or abstract class) for both real objects and decorators. Defines operation(). |
| ConcreteComponent | The “core” object that gets decorated — it does the base work and holds no wrappee. |
| Decorator | Abstract wrapper holding a reference to a Component. Forwards calls to it; subclasses add behavior before/after/around forwarding. |
| ConcreteDecorator | Adds the actual extra responsibility (compression, logging, rate-limiting, …). |
The Decorator base class itself adds nothing — it exists only to hold the wrappee and forward by default, so concrete decorators override only the methods they care about.
A Real-World Example: A Notification Pipeline
The textbook examples are coffee + condiments and Java’s InputStream. They’re fine, but let’s use something you’d actually build at SDE-1: a notification sender where each cross-cutting concern is an independent, runtime-configurable layer.
The core Notifier sends a message. We want to optionally layer on three concerns, one of each timing flavor so you can see the full taxonomy:
- Signing — transforms the payload before delegating.
- Logging — records the receipt after the inner call returns (true post-processing).
- Retry — wraps the delegated call in control flow (around).
Each is orthogonal; product wires different combinations per channel.
The sign() method below is a toy h*31 rolling hash purely so the example is self-contained and deterministic — it is not a real signature. Never ship a rolling hash as a signature: it is trivially forgeable and gives zero integrity guarantee. In production you HMAC the payload with a real key (e.g., crypto.createHmac('sha256', key), hmac.new(key, msg, sha256), or libsodium/OpenSSL in C++).
// Component
interface Notifier {
send(message: string): string; // returns delivery receipt
}
// ConcreteComponent
class EmailNotifier implements Notifier {
constructor(private readonly addr: string) {}
send(message: string): string {
return `email->${this.addr}: ${message}`;
}
}
// Decorator (abstract): holds the wrappee, forwards by default
abstract class NotifierDecorator implements Notifier {
constructor(protected readonly inner: Notifier) {}
send(message: string): string {
return this.inner.send(message);
}
}
// ConcreteDecorator: transform the input BEFORE delegating
class SigningNotifier extends NotifierDecorator {
constructor(inner: Notifier, private readonly key: string) { super(inner); }
send(message: string): string {
const sig = this.sign(message);
return this.inner.send(`${message} | sig=${sig}`);
}
private sign(m: string): string {
// TOY hash for a deterministic demo — NOT a real HMAC. Do not ship this.
let h = 0;
for (const c of this.key + m) h = (h * 31 + c.charCodeAt(0)) | 0;
return (h >>> 0).toString(16);
}
}
// ConcreteDecorator: post-process AFTER the inner call returns
class LoggingNotifier extends NotifierDecorator {
send(message: string): string {
const receipt = this.inner.send(message); // delegate first...
console.error(`[log] delivered, receipt=${receipt}`); // ...then act on the result
return receipt;
}
}
// ConcreteDecorator: wrap the call (AROUND / retry)
class RetryNotifier extends NotifierDecorator {
constructor(inner: Notifier, private readonly attempts: number) {
super(inner);
if (attempts < 1) throw new Error("attempts must be >= 1");
}
send(message: string): string {
let lastErr: unknown;
for (let i = 0; i < this.attempts; i++) {
try { return this.inner.send(message); }
catch (e) { lastErr = e; }
}
throw lastErr; // all attempts exhausted
}
}
// Compose at runtime — order matters!
const notifier: Notifier =
new RetryNotifier(
new SigningNotifier(
new EmailNotifier("ops@x.com"),
"secret"),
3);
console.log(notifier.send("disk full"));
// email->ops@x.com: disk full | sig=95673682All three RetryNotifiers now have the same shape: loop attempts times, capture the last error (lastErr / last_err / std::exception_ptr), and rethrow on exhaustion — with an identical fail-fast guard (attempts < 1 throws) so the zero/negative boundary behaves the same in every language. The happy path prints email->ops@x.com: disk full | sig=95673682 in all three.
Notice the three idioms that define a decorator:
- It holds the wrappee (
inner/inner_) typed as the interface, never the concrete class. - It implements the same interface, so it slots in wherever a
Notifieris expected. - It can act before, after, or around the delegated call —
SigningNotifiertransforms the input before forwarding;LoggingNotifierpost-processes after the result returns;RetryNotifierwraps the call around control flow.
Order is semantics, not just style. Retry(Sign(email)) signs once and retries delivery; Sign(Retry(email)) would re-sign on every retry. In an interview, explicitly state the ordering you chose and why — it shows you understand that decorators compose like function composition, not like a commutative set.
Why both the coffee and IO-stream examples are “the same shape”
- Coffee/condiments:
Beverageis the Component;Espressois the ConcreteComponent;MilkDecorator,MochaDecoratorwrap and add tocost()anddescription().Mocha(Milk(Espresso()))sums costs by delegating toinner.cost()then adding its own. - Java IO:
new BufferedReader(new InputStreamReader(new FileInputStream(f))). Each layer is a decorator overReader/InputStream, adding buffering, charset decoding, etc. This is the most famous production-grade Decorator hierarchy in existence — and the reasonInputStreamhas manyFilterInputStreamsubclasses rather than one mega-class. - Web middleware: Express (
app.use((req, res, next) => …)) and Python ASGI middleware are Decorator implemented with closures instead of classes — each middleware wrapsnext/the inner app, can act before and after it, and composes into a chain. Same pattern, noclasskeyword required.
The notification pipeline, the coffee, the IO streams, and middleware are structurally identical: same interface up and down the chain, each layer delegating to its inner and adding one concern.
Why Decorator Embodies the Open/Closed Principle
The Open/Closed Principle says software entities should be open for extension but closed for modification. Decorator is arguably the cleanest illustration in the catalog:
- Closed for modification:
EmailNotifieris never touched when you add signing, retry, logging, or rate-limiting. The core class is frozen. - Open for extension: a new concern = a new
NotifierDecoratorsubclass. Existing code, existing tests, existing wiring all keep working.
Contrast with editing EmailNotifier to add an if (sign) {...} flag for every feature — that violates OCP (you modify a stable class for each new requirement) and produces a flag-soup constructor with exponentially many untestable branches.
Decorator also supports Single Responsibility (each decorator owns exactly one concern) and depends on the interface, honoring the Dependency Inversion Principle.
SSE-Grade Concerns: Threads, Indirection, and the Real Cost
At SDE-1→SSE level, interviewers probe whether you understand the cost of the pattern, not just its shape.
Thread-safety of stateful decorators
A stateless decorator (signing, logging, retry) is trivially thread-safe — it owns no mutable state, so concurrent send() calls don’t interfere. A stateful decorator is where bugs live. A BufferedNotifier that batches messages, or a CacheDecorator that memoizes responses, holds mutable state shared across threads. Its read-modify-write (check buffer → append, or check cache → miss → populate) is a race unless guarded by a std::mutex / lock / synchronized block or backed by a concurrent data structure.
A stateful decorator is the part of the chain that silently breaks under concurrency. When you introduce one, say so out loud in an interview and name your synchronization strategy. A common refinement for a cache decorator is to perform the expensive inner call outside the lock and double-check on insert, so you don’t hold the lock across I/O.
The cost of deep wrappee indirection
Every layer is an extra indirect call:
- C++: each
inner_->send(...)is a virtual dispatch — a vtable lookup the optimizer usually can’t inline across the chain. A 6-deep stack is 6 virtual calls plus pointer-chasing per request. - JIT’d languages (V8, JVM): a call site that sees many different decorator types becomes megamorphic, and the JIT gives up on inlining/speculation, deoptimizing the hot path.
For most code this is noise. On a hot path (per-byte stream codecs, per-request RPC middleware at high QPS), a deep decorator chain is a real cost — collapse fixed combos into one class or hoist the chain out of the inner loop. Deep stacks also produce opaque stack traces, which is its own debugging tax.
When to Use It
- You need to add responsibilities to individual objects dynamically and transparently, without affecting other objects of the same class.
- Responsibilities are orthogonal and stackable — any subset, any order.
- Subclassing is impractical because of combinatorial explosion, or the class is
final/sealed, or the set of behaviors is open-ended. - You want to add/remove behavior at runtime (e.g., enable encryption only for certain tenants).
Classic fits: middleware chains (HTTP handlers wrapping next), stream/codec layers, UI scroll/border decorations, instrumentation (metrics/tracing wrappers around a repository).
When NOT to Use It (Pitfalls)
Identity & type-checking break. A decorated object is not instanceof ConcreteComponent in a useful sense, and decorated === original is false. Any code that does downcasts, reference-equality, or getClass() checks will misbehave. Decorator assumes clients only ever talk to the interface.
The canonical mitigation for the identity pitfall: program strictly to the interface and never downcast or compare references. When you genuinely must introspect the chain, expose a deliberate hook on the abstract decorator rather than relying on instanceof:
abstract class NotifierDecorator implements Notifier {
constructor(protected readonly inner: Notifier) {}
send(message: string): string { return this.inner.send(message); }
unwrap(): Notifier { return this.inner; } // walk the chain on purpose
}
Now a caller that needs the core can walk unwrap() to the bottom intentionally, instead of guessing types. (The .NET Stream/DbConnection families and many resilience libraries expose exactly this kind of “inner” accessor.)
- Deep stacks are hard to debug. A 6-layer chain produces opaque stack traces and confusing ordering bugs. If you have a fixed, finite set of feature combos, a couple of subclasses or a Builder may be clearer.
- Order-dependence is a footgun. Because layers aren’t commutative, wiring them wrong is a silent logic bug, not a compile error. Centralize construction in a factory/builder.
- Interface bloat. If the Component interface is wide, every decorator must forward every method correctly — miss one and you get a subtle “transparency leak.” Keep the decorated interface narrow.
- Don’t reach for it when you have one behavior. If there’s exactly one optional concern, a flag or a Strategy is simpler. Decorator earns its keep with combinatorics.
Decorator vs. Its Siblings
Decorator vs. Inheritance
| Inheritance | Decorator | |
|---|---|---|
| Binding time | Compile time, static | Runtime, dynamic |
| Combinations of n features | ~2ⁿ−1 extra classes | n decorators, composed |
| Per-instance customization | No (all instances of a class identical) | Yes (each object wrapped differently) |
| Relationship | is-a | is-a and has-a |
Decorator is “inheritance done with composition.” It trades a rigid class hierarchy for runtime flexibility.
Decorator vs. Strategy
Both extend behavior via composition, and both are easy to confuse — interviewers love this.
- Strategy changes the guts of an object: a host object delegates one variable step to a pluggable algorithm behind a (usually narrower)
Strategyinterface — e.g., aSorterthat takes aComparisonStrategy. The host stays one object; you swap what it does internally. Strategies are not stacked and are typically not the same interface as the host. - Decorator changes the skin of an object: it wraps an object that shares the full
Componentinterface and is itself aComponent, so decorators stack recursively into a chain. You add outer responsibilities, you don’t replace an inner step.
One-line interview answer: Strategy plugs one swappable algorithm into an object; Decorator wraps an object with same-interface layers that compose. Strategy is a single injected step; Decorator is a recursive chain.
Decorator vs. Proxy
Same structure (wrapper holds a same-interface wrappee), different intent. A Proxy controls access to the wrappee — lazy loading, access control, remoting, caching — and usually manages the wrappee’s lifecycle itself; the client doesn’t choose to wrap it. A Decorator adds responsibilities and is composed deliberately by the client, often many-deep. If your wrapper’s job is “should I even let you reach the real thing / when do I create it,” it’s a Proxy; if it’s “let me add behavior on the way through,” it’s a Decorator.
Decorator vs. Adapter
An Adapter changes the interface — it makes an incompatible class fit an interface the client expects. A Decorator keeps the interface identical. Different interface → Adapter; same interface → Decorator.
Decorator vs. Chain of Responsibility
Both are linked wrappers. The difference is completion semantics: in CoR each handler may stop the chain (handle-and-return without delegating), so not every link runs. In Decorator every layer normally delegates to its inner — the whole chain participates in producing the result. CoR is “find the one who handles it”; Decorator is “everyone contributes.”
All four siblings (Decorator, Proxy, Adapter, Chain of Responsibility) can look like “an object holding another object behind an interface.” Disambiguate by intent: add responsibilities (Decorator) vs. control access (Proxy) vs. convert interface (Adapter) vs. pass-until-handled (CoR). Say the intent out loud in an interview — that’s the signal you actually understand the catalog.