Decorator Pattern
The Decorator pattern attaches new behavior to an object dynamically by wrapping it in another object that shares its interface. Use it when you want to add cross-cutting features — logging, caching, validation — without subclassing or polluting the base type.
Overview
The Decorator pattern wraps an object inside another object that implements the same interface, layering extra behavior on top. Unlike inheritance, decoration happens at runtime and can be combined freely — the same base service can be wrapped with a logger, then a cache, then a retry policy, in any order. It is the foundation for things like Express middleware and React higher-order components.
When to use it
- You need to add cross-cutting concerns (logging, caching, auth, retry) without changing the wrapped class.
- The set of features you want to combine is open and varies by call site.
- Subclassing would create a combinatorial explosion of classes (LoggedCachedRetryingService...).
Example
interface DataService {
fetch(id: string): Promise<string>;
}
class HttpDataService implements DataService {
async fetch(id: string) { return `data:${id}`; }
}
class LoggingDecorator implements DataService {
constructor(private readonly inner: DataService) {}
async fetch(id: string) {
console.log(`fetch start ${id}`);
const result = await this.inner.fetch(id);
console.log(`fetch end ${id}`);
return result;
}
}
class CachingDecorator implements DataService {
private cache = new Map<string, string>();
constructor(private readonly inner: DataService) {}
async fetch(id: string) {
const cached = this.cache.get(id);
if (cached) return cached;
const result = await this.inner.fetch(id);
this.cache.set(id, result);
return result;
}
}
const service = new LoggingDecorator(
new CachingDecorator(new HttpDataService())
);Pros
- Compose behavior at runtime without touching the original class.
- Open/closed: add new decorators without modifying existing code.
- Each concern lives in its own class — easy to test in isolation.
Cons
- Stacks of decorators can be hard to debug (which layer added that behavior?).
- Order of composition matters and is easy to get wrong.
- Identity is fragmented — decorated !== original, which can break equality checks.