Decorator Pattern
Decorator wraps an object that shares its interface and adds behaviour before/after delegating to the original.
Introduction
Decorator wraps an object that shares its interface and adds behaviour before/after delegating to the original. Stack decorators at runtime: HttpClient wrapped by RetryClient, then LoggingClient, then MetricsClient — each adds one concern without subclassing the others.
The story
A team built four FileWriter subclasses: Buffered, Compressed, Encrypted, and EncryptedCompressedBuffered. Combinatorial explosion was visible. They refactored to stackable decorators — three orthogonal wrappers composing in any order.
The business problem
Optional behaviour combinations explode with inheritance:
- Subclass explosion: retry × log × metric × cache = 2ⁿ classes.
- Rigid composition: can't add logging without modifying base class.
- Environment variance: test needs fakes; prod needs metrics — subclass tree can't flex.
- Open/Closed violation: new concern means new subclass of every existing variant.
The problem teams faced
Decorator fits when:
- Optional behaviours combine in many ways (retry × log × cache).
- You need to add a concern without modifying the class or callers.
- Composition must vary per environment (test, dev, prod) via config.
- Same interface must be preserved — callers unaware of wrappers.
Understanding the topic
Intent: Attach additional responsibilities dynamically. Flexible alternative to subclassing.
- Component — interface for objects that can be wrapped.
- ConcreteComponent — base object doing core work.
- Decorator — holds Component, forwards by default, adds pre/post behaviour.
- Composition root — chains wrappers: Metrics(Logging(Retry(Base))).
Internal architecture
Stackable HTTP client decorators:
interface HttpClient {fetch(url: string, opts?: RequestInit): Promise<Response>}class RetryDecorator implements HttpClient {constructor(private inner: HttpClient, private max = 3) {}async fetch(url: string, opts?: RequestInit) {for (let i = 0; i < this.max; i++) {try { return await this.inner.fetch(url, opts) }catch (e) { if (i === this.max - 1) throw e }}throw new Error("unreachable")}}// Composition rootconst client = new MetricsDecorator(new LoggingDecorator(new RetryDecorator(new FetchHttpClient())))
Visual explanation
Three diagrams cover decorator chain, vs inheritance, and Express middleware:
Informative example
Before / after — subclass explosion to decorator chain:
// ❌ Combinatorial subclassesclass EncryptedCompressedBufferedFileWriter extends FileWriter { /* ... */ }// ✅ Stackable decorators — any order, any subsetinterface Writer { write(data: Buffer): void }class BufferedWriter implements Writer {constructor(private inner: Writer) {}write(data: Buffer) { /* buffer then */ this.inner.write(data) }}class EncryptedWriter implements Writer {constructor(private inner: Writer) {}write(data: Buffer) { this.inner.write(encrypt(data)) }}const writer = new EncryptedWriter(new BufferedWriter(new FileWriter(path)))// Functional form (Express-style)const withLogging = (fn: Handler): Handler => (req, res, next) => {console.log(req.path); return fn(req, res, next)}
Execution workflow
Define Component
Interface with the operation clients call.
Real-world use
Java I/O streams (BufferedInputStream wrapping FileInputStream), Express/Koa middleware, Spring AOP advice chains, OkHttp interceptors, Python @decorator syntax, React HOCs, Java Servlet filters.
Production case study
HTTP client cross-cutting stack:
- Scenario: 12 services each reimplemented retry, logging, metrics on outbound HTTP.
- Decision: Shared decorator chain configured at composition root per service.
- Outcome: New concern (circuit breaker) = one decorator class, zero service edits.
- Metric: duplicated HTTP wrapper code eliminated across 12 repos.
Trade-offs
- Pro: compose features at runtime without subclass explosion.
- Pro: Open/Closed — new decorator, no base class edits.
- Con: deep chains hard to debug — order and nesting matter.
- Con: many small wrapper objects — negligible unless extreme depth.
Decision framework
- Use when optional behaviours combine in many ways.
- Use when same interface must be preserved for callers.
- Avoid when single fixed behaviour — just implement it once.
- Avoid when interface should change — that's Adapter, not Decorator.
Best practices
- One concern per decorator — retry, log, metrics are separate classes.
- Document decorator order at composition root — outermost runs first.
- Functional decorators (HOFs) in TS/JS when classes add no value.
- Keep Component interface stable — decorators must not leak wrapper types.
Anti-patterns to avoid
- Mega-decorator doing retry + log + cache + auth in one class.
- Decorator changing the interface — that's Adapter.
- 20-layer chain with no documentation of order.
Common mistakes
- Wrong order — logging inside retry logs every retry attempt (may be intended or not).
- Decorator holding mutable state shared across calls.
- Double-wrapping same concern — metrics inside metrics.
Debugging tips
- Log decorator entry/exit with depth indent — trace chain execution.
- Temporarily remove outer decorators to isolate which layer breaks.
Optimization strategies
- Skip expensive decorators in hot test paths.
- Short-circuit decorators that noop when feature flag off.
Common misconceptions
- Decorator ≠ Proxy. Decorator adds behaviour; Proxy controls access/lazy-init with same role.
- Express middleware is Decorator — no classes required.
- Decorator preserves interface; Adapter changes it.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionDecorator vs inheritance?+
Answer
Follow-up
2AdvancedQuestionDecorator vs Proxy?+
Answer
Follow-up
3IntermediateQuestionImplement Decorator without classes?+
Answer
Follow-up
4AdvancedQuestionDoes decorator order matter?+
Answer
Follow-up
Summary
You can replace combinatorial subclasses with stackable decorators and explain order semantics. Tell the FileWriter explosion story — if you can do that, you own Decorator.