Decorator & Chain of Responsibility
Layering behavior by wrapping objects that share an interface β the pattern behind middleware pipelines, composable cross-cutting concerns, and short-circuiting handler chains.
The Decorator pattern adds behavior to an object by wrapping it in another object with the same interface, stacking responsibilities like the layers of an onion. The closely related Chain of Responsibility passes a request through a sequence of handlers, each able to act or pass it on. Both favor composition over inheritance and underpin the middleware pipelines you use every day.
The Decorator pattern adds behavior to an object by wrapping it in another object that shares the same interface. Because the wrapper implements the interface too, callers can't tell the difference β they just see a DataService. You add responsibilities dynamically at composition time instead of via a class explosion of subclasses.
Each decorator does a little work (before and/or after), then delegates to the inner object it wraps. Stack several and you get an onion: a call passes inward through each layer to the core, and the result passes back out through each layer β exactly like middleware.
A close cousin: a request passes along a chain of handlers, each deciding whether to handle it or pass it on. A handler can short-circuit the chain β e.g. a warm CachingDecorator returns immediately and the core is never reached, or an auth handler rejects before anything else runs.
Logging, caching, retry, compression, and authorization each become a single-responsibility decorator. You compose only the ones you need, in the order you need them β order matters (cache-then-log differs from log-then-cache). The core stays oblivious, and concerns don't entangle each other.
Key Concepts
An object that wraps another with the same interface, adding behavior before/after delegating inward.
Decorator and core share an interface, so wrappers are transparent to callers and infinitely stackable.
A request flows through a chain of handlers; each can handle, transform, or pass it along β or short-circuit.
Add behavior by combining wrappers at runtime instead of a subclass for every combination.
The sequence of decorators changes behavior (cacheβlog vs logβcache). Composition is explicit.
Web middleware pipelines are the decorator/chain pattern applied to HTTP requests.
1// Decorator: wrap an object in another with the SAME interface.2interface DataService { fetch(id: number): Promise<Data>; }34class CoreDataService implements DataService {5 async fetch(id) { return db.query(id); } // the real work6}78class LoggingDecorator implements DataService {9 constructor(private inner: DataService) {} // wraps another DataService10 async fetch(id) {11 console.log("β", id);12 const r = await this.inner.fetch(id); // delegate inward13 console.log("β", id);14 return r;15 }16}1718class CachingDecorator implements DataService {19 private cache = new Map<number, Data>();20 constructor(private inner: DataService) {}21 async fetch(id) {22 if (this.cache.has(id)) return this.cache.get(id)!; // short-circuit23 const r = await this.inner.fetch(id);24 this.cache.set(id, r);25 return r;26 }27}2829// Compose like an onion β order matters:30const service = new LoggingDecorator(new CachingDecorator(new CoreDataService()));
Decorator and Chain of Responsibility are everywhere: ASP.NET/Express middleware, HTTP client handlers (HttpClientFactory's DelegatingHandler), stream wrappers, and DI interceptors are all this pattern. Recognizing it lets you add caching, logging, retry, or auth as clean, composable layers instead of tangling them into core logic β a frequent design-interview and real-world refactoring tool.
Common Pitfalls
1The Subclass Combinatorial Explosion
A NotificationSender needs optional encryption, compression, and retry β in any combination. Modeling it with inheritance produces EncryptedSender, CompressedSender, EncryptedCompressedSender, EncryptedCompressedRetrySenderβ¦ a class for every combination.
Every new optional behavior doubles the number of subclasses needed to cover the combinations. The hierarchy becomes unmanageable, and behaviors can't be added or removed at runtime.
Make each behavior a decorator wrapping a base sender: new RetryDecorator(new CompressionDecorator(new EncryptionDecorator(core))). Compose exactly the behaviors you want, in the order you want, at runtime β three decorators cover all eight combinations.
Takeaway: When features combine freely, decorators replace a combinatorial subclass explosion with a handful of composable wrappers. Behavior is assembled at runtime instead of frozen into a class hierarchy.
2Adding Caching Without Touching the Service
An expensive third-party API client is used in dozens of places. You want to add caching and retry, but you can't (and shouldn't) modify the client, and editing every call site is error-prone.
Cross-cutting concerns like caching and retry would have to be bolted onto the client or duplicated at every call site, coupling unrelated logic and risking inconsistency.
Wrap the client in CachingDecorator and RetryDecorator that implement the same interface, and inject the decorated instance via DI. Every existing call site transparently gets caching and retry, while the original client and the callers stay completely untouched.
Takeaway: Decorators let you layer behavior onto code you can't or shouldn't modify. Wrap the dependency, swap it in at the composition root, and the whole app benefits without a single call-site change.
Join the discussion
Comments
Loading comments...
Cookie preferences
We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.