Decorator Pattern: Add Capability in Layers
Decorator wraps an object with optional layers, allowing capabilities to be combined at runtime without creating a subclass for every combination.
What is the Decorator pattern?
The Decorator pattern is a structural design pattern that attaches new responsibilities to an object by wrapping it in another object that implements the same interface. Each wrapper adds exactly one capability — encryption, buffering, logging, retry — and then delegates to the object underneath. Because wrapper and wrapped share a contract, layers stack in any combination at runtime.
Decorator exists to kill the subclass explosion. With four optional features, inheritance needs up to sixteen subclasses to cover the combinations; decorators need four small classes composed as needed. Java's I/O streams (`BufferedInputStream(new FileInputStream(...))`) and middleware pipelines are decorator chains you have probably already used.
The logic, step by step
Optional features multiply into a subclass explosion because every possible combination requires another specialized type.
Wrap the same interface with focused decorators that add one responsibility before or after delegation.
- BASE OBJECT provides the smallest valid behavior behind one interface.
- DECORATORS wrap that interface and add a focused responsibility.
- CALLS flow through the selected layers and finally reach the base.
Features become composable layers rather than a rigid inheritance tree full of combinations.
Decorator pattern example in TypeScript
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string): void {
smtp.deliver(message);
}
}
class EncryptedNotifier implements Notifier {
constructor(private inner: Notifier) {}
send(message: string): void {
this.inner.send(encrypt(message)); // add one concern, then delegate
}
}
class ThrottledNotifier implements Notifier {
private last = 0;
constructor(private inner: Notifier, private minGapMs: number) {}
send(message: string): void {
const now = Date.now();
if (now - this.last < this.minGapMs) return;
this.last = now;
this.inner.send(message);
}
}
// Compose only the layers this deployment needs:
const notifier: Notifier =
new ThrottledNotifier(new EncryptedNotifier(new EmailNotifier()), 5000);
notifier.send('Balance updated'); Run it in the real world
A notification begins as plain text, then gains encryption, compression, and logging only where those layers are required.
When to use it — and when not to
Reach for Decorator when
- Optional capabilities need to combine freely (compression + encryption + logging, in any subset).
- You must add behavior to objects you cannot or should not subclass, including third-party classes.
- Responsibilities should be attachable and removable at runtime rather than fixed at compile time.
Skip it when
- The stack of wrappers is fixed and global — a single class or a function pipeline is simpler.
- Consumers need to reach a specific layer's API; digging through wrappers breaks the abstraction.
Decorator vs related patterns
Adapter pattern — Adapter changes an interface into a different one; Decorator keeps the interface identical and adds behavior. If the shape changes, it is an adapter; if the shape survives, it is a decorator.
Facade pattern — Facade simplifies many objects behind one new interface; Decorator enriches one object behind its existing interface.
Frequently asked questions
What is a decorator pattern in simple terms?
It is gift-wrapping for objects: each layer of wrapping adds one feature and passes the work inward, and the outside world still sees the same shape it started with.
Are TypeScript/Python @decorators the same as the Decorator pattern?
They are related but not identical. Language-level decorators are syntax that transforms a class or function at definition time. The design pattern composes wrapper objects at runtime. You can implement the pattern with the syntax, but most @decorators do other things.
When does the Decorator pattern become a problem?
Deep chains make debugging painful — a stack trace through six wrappers obscures where behavior lives, and identity checks (instanceof, equality) break because the outer object is not the inner one.