HT
How Things Work
System Online

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.

How It Works

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.

1
Wrap, Don't Subclass

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.

2
Delegate Inward

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.

3
Chain of Responsibility

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.

4
Composable Cross-Cutting Concerns

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

🎁Decorator

An object that wraps another with the same interface, adding behavior before/after delegating inward.

πŸ”—Same Interface

Decorator and core share an interface, so wrappers are transparent to callers and infinitely stackable.

⛓️Chain of Responsibility

A request flows through a chain of handlers; each can handle, transform, or pass it along β€” or short-circuit.

🧱Composition over Inheritance

Add behavior by combining wrappers at runtime instead of a subclass for every combination.

πŸ”’Order Matters

The sequence of decorators changes behavior (cache→log vs log→cache). Composition is explicit.

πŸ§…Middleware

Web middleware pipelines are the decorator/chain pattern applied to HTTP requests.

Composable decorators
tsx
1// Decorator: wrap an object in another with the SAME interface.
2interface DataService { fetch(id: number): Promise<Data>; }
3
4class CoreDataService implements DataService {
5 async fetch(id) { return db.query(id); } // the real work
6}
7
8class LoggingDecorator implements DataService {
9 constructor(private inner: DataService) {} // wraps another DataService
10 async fetch(id) {
11 console.log("β†’", id);
12 const r = await this.inner.fetch(id); // delegate inward
13 console.log("←", id);
14 return r;
15 }
16}
17
18class 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-circuit
23 const r = await this.inner.fetch(id);
24 this.cache.set(id, r);
25 return r;
26 }
27}
28
29// Compose like an onion β€” order matters:
30const service = new LoggingDecorator(new CachingDecorator(new CoreDataService()));
πŸ’‘
Why This Matters

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

⚠Wrong decorator order β€” e.g. logging outside caching logs even cache hits; caching outside auth caches unauthorized results.
⚠Deep wrapper stacks that make debugging hard β€” a stack trace through ten decorators is painful.
⚠Decorators that change the interface or break the Liskov contract, so they're no longer transparent.
⚠Putting heavy business logic in a decorator instead of cross-cutting concerns β€” decorators should be thin.
⚠Forgetting a chain handler can short-circuit, leading to surprise when the core never runs (or always does).
Real-World Use Cases

1The Subclass Combinatorial Explosion

Scenario

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.

Problem

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.

Solution

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

Scenario

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.

Problem

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.

Solution

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.