Coding · Design patterns
Decorator pattern explained with examples
Short answer
Decorator is a structural design pattern that adds responsibilities to an object dynamically by wrapping it in another object with the same interface. Each decorator forwards calls to the wrapped object and adds its own behavior before or after, such as logging, caching or compression. Because decorators can be stacked in any order, they are a flexible alternative to creating a subclass for every combination of features.
Intent
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. Decorator is one of the seven structural patterns in the Gang of Four (GoF) book, where it is also known as Wrapper.
The problem it solves
Imagine a data store with get(key). Some callers want logging, some want an in-memory cache, some want both, and some want retries for a slow network back end. With inheritance you would write LoggingStore, CachingStore, LoggingCachingStore, RetryingCachingStore and so on: one subclass for every combination, and the combinations are fixed at compile time.
Decorator turns each feature into a small wrapper class. You build the combination you need at run time by wrapping one object in another, and the code that uses the store sees the same interface no matter how many layers there are.
Structure and participants
| Participant | Responsibility | In the example |
|---|---|---|
| Component | The interface shared by real objects and decorators. | Store |
| ConcreteComponent | The original object that does the core work. | SlowStore |
| Decorator | Holds a reference to a Component and implements the same interface by forwarding. | StoreDecorator |
| ConcreteDecorator | Adds behavior before or after forwarding. | LoggingStore, CachingStore |
TypeScript implementation
interface Store {
get(key: string): string;
}
class SlowStore implements Store {
get(key: string): string { console.log(` (database read: ${key})`); return key.toUpperCase(); }
}
// Base decorator: forwards everything to the wrapped store.
abstract class StoreDecorator implements Store {
constructor(protected inner: Store) {}
get(key: string): string { return this.inner.get(key); }
}
class LoggingStore extends StoreDecorator {
get(key: string): string {
console.log(`get ${key}`);
return super.get(key);
}
}
class CachingStore extends StoreDecorator {
private cache = new Map<string, string>();
get(key: string): string {
if (!this.cache.has(key)) this.cache.set(key, super.get(key));
return this.cache.get(key)!;
}
}
// Stack features in any order at run time.
const store: Store = new LoggingStore(new CachingStore(new SlowStore()));
console.log(store.get("ada"));
console.log(store.get("ada"));
// get ada
// (database read: ada)
// ADA
// get ada
// ADAThe second call is logged but answered from the cache, so the database is not read again. If you swap the order, new CachingStore(new LoggingStore(...)), only cache misses are logged. The order of decorators changes the behavior, which is both a feature and a common source of confusion.
Function decorators in TypeScript
When the component is a single function, a decorator can be a higher-order function: it takes a function and returns a new function with the same signature and extra behavior.
function withTiming<A extends unknown[], R>(name: string, fn: (...args: A) => R) {
return (...args: A): R => {
const start = performance.now();
try {
return fn(...args);
} finally {
console.log(`${name} took ${(performance.now() - start).toFixed(1)} ms`);
}
};
}
const slowSquare = (n: number) => { for (let i = 0; i < 1e6; i++); return n * n; };
const timedSquare = withTiming("square", slowSquare);
console.log(timedSquare(12)); // square took ... ms, then 144When to use it, and when not to
Use Decorator when:
- You want to add features such as logging, caching, retries, validation, compression or access checks to individual objects, without affecting other objects of the same class.
- Features must be combined freely, and subclassing would produce a class for every combination.
- You cannot or should not change the original class, for example because it comes from a library.
Avoid it when:
- You need only one fixed extra behavior. Adding it directly, or using one subclass, is simpler.
- Code depends on the object's identity or concrete class. A decorated object is a different object, so checks such as
instanceof SlowStoreor===comparisons fail. - Many small layers would make debugging hard. Deep stacks of wrappers produce long call stacks and make it unclear which layer did what.
Middleware in web frameworks, such as Express or Koa, is a related idea: each layer receives a request, may act on it, and passes it on.
Real-world uses
- Java I/O:
new BufferedInputStream(new FileInputStream("data.bin"))wraps a stream in another stream that adds buffering, and both have theInputStreaminterface. The GoF book itself discusses streams as a typical example. - Java collections:
Collections.unmodifiableList(list)andCollections.synchronizedList(list)return wrappers that add read-only behavior or locking around an existing list. - Python: function decorators such as
functools.lru_cachewrap a function and add caching, andfunctools.wrapshelps a wrapper keep the wrapped function's name and docstring.
Related patterns
| Pattern | How it differs from Decorator |
|---|---|
| Adapter | Changes the interface of an object. A decorator keeps the interface and adds behavior. |
| Proxy | Has the same structure but a different purpose: it controls access (lazy creation, remote access, permissions), and usually manages the real object's life cycle itself. |
| Composite | Also uses recursive composition, but a composite holds many children; a decorator wraps exactly one. |
| Strategy | Changes the inside of an object by swapping an algorithm. A decorator changes the outside by wrapping it. |
| Chain of Responsibility | Also passes requests through a series of objects, but each handler may stop the request; decorators normally always forward. |
FAQ
What is the difference between Decorator and inheritance?
Inheritance adds behavior to a whole class at compile time. A decorator adds behavior to one object at run time, and several decorators can be combined in any order.
What is the difference between Decorator and Proxy?
They have the same shape. A decorator adds features that the client chooses to stack, while a proxy controls access to the real object, often creating it or deciding whether to call it at all.
Are TypeScript @decorators the Decorator pattern?
Not exactly. @ decorators are a language feature that modifies classes and their members when they are defined. They can be used to implement wrapping, but the GoF pattern is about wrapping objects at run time.
Does the order of decorators matter?
Yes. Each decorator sees the calls before the objects it wraps, so a cache placed outside a logger hides repeated calls from the logger, while a logger outside the cache sees every call.