LESSON 5.4
Decorator
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
15 min read
Real-world analogy
When dressing for freezing weather, you do not manufacture a single specialized "t-shirt-sweater-raincoat" garment. Instead, you put on a base t-shirt, wrap it with a sweater for warmth, and wrap the sweater with a waterproof jacket for rain protection. Each layer conforms to your body, adds a specific capability, and can be removed or combined in any order depending on conditions.
The problem
You are designing an alerting notification pipeline. Initially, the system only sends basic email notifications. Soon, stakeholders demand SMS alerts, Slack alerts, and PagerDuty escalations. Furthermore, different alert types require different combinations:
- Critical alerts: Email + SMS + PagerDuty
- Warning alerts: Email + Slack
- Info alerts: Email only
If you use class inheritance to support every combination, you quickly suffer a combinatorial class explosion ($2^N$ subclasses):
Notifier
├── EmailNotifier
├── SMSNotifier
├── SlackNotifier
├── EmailSMSNotifier
├── EmailSlackNotifier
├── EmailSMSAndSlackNotifier
└── EmailSMSAndPagerDutyNotifierStatic inheritance forces you to anticipate every permutation at compile time. It is impossible to add an SMS alert to an existing notifier at runtime based on user preferences.
The solution
The Decorator pattern attaches additional responsibilities to an object dynamically using composition. Decorators wrap the original component, implement the exact same interface, and delegate calls to the wrapped object while executing additional logic before or after delegation.
- Component (
Notifier): Common interface implemented by both concrete components and decorators. - Concrete Component (
BasicNotifier): The base object that provides default core functionality. - Base Decorator (
NotifierDecorator): Holds a reference to a wrappedNotifierand delegates all calls to it. - Concrete Decorators (
SMSDecorator,SlackDecorator): Adds specific responsibilities dynamically.
Step-by-step implementation
Step 1: Define the component interface
Both the base notifier and all decorators must conform to this contract.
class Notifier {
send(message) {
throw new Error("send() must be implemented");
}
}public class Example {
interface Notifier {
void send(String message);
}
Step 2: Implement the concrete component
Provide the basic standalone implementation (e.g. standard email).
class Notifier {
send(message) { throw new Error("not implemented"); }
}
class EmailNotifier extends Notifier {
send(message) {
console.log(`[Email] ${message}`);
}
}
Step 3: Implement base and concrete decorators
The base decorator delegates to the wrapped instance. Concrete decorators add behavior.
The complete example
Here is the complete runnable implementation demonstrating stacking decorators dynamically at runtime.
--- Pipeline 1: Email + SMS ---
Email to ops@example.com: Disk usage exceeded 85%
SMS to +1-555-0199: Disk usage exceeded 85%
--- Pipeline 2: Timestamped Email + Slack + SMS ---
Email to ops@example.com: [2026-10-19T10:00:00.000Z] Database replica lag > 60s
SMS to +1-555-0142: [2026-10-19T10:00:00.000Z] Database replica lag > 60s
Slack post to #prod-incidents: [2026-10-19T10:00:00.000Z] Database replica lag > 60sVariations
Stacking and execution ordering
Because decorators form an onion-like wrapping chain, their composition order directly impacts runtime execution:
- Pre-processing: Outer decorators execute their pre-processing code before passing the argument to inner decorators.
- Post-processing: Outer decorators receive the return value and execute their post-processing code after inner decorators finish.
For example, when chaining EncryptionDecorator and CompressionDecorator, compressing first and encrypting second yields high compression efficiency. Encrypting first produces high-entropy pseudo-random ciphertext that cannot be compressed.
Higher-order functions in JavaScript
In modern JavaScript and functional programming, the Decorator pattern is frequently implemented as higher-order functions rather than class hierarchies. A higher-order function takes a function as an argument and returns a new function with added telemetry, rate limiting, or caching.
Where you've already used it
java.iostream hierarchy:Snippet.javaInputStream stream = new GZIPInputStream( new BufferedInputStream( new FileInputStream("archive.tar.gz") ) );Java · 5 linesUTF-8FileInputStreamreads bytes;BufferedInputStreamadds buffer pooling;GZIPInputStreamdecompresses the byte stream dynamically.- Express / Koa middleware: Express handler functions
(req, res, next)wrap request processing in sequential decorator layers (authentication, body parsing, CORS). - Spring AOP &
@Transactional: Spring creates dynamic proxies that decorate service methods with transaction begin, commit, and rollback logic.
Pros and cons
Advantages
- Avoids Class Explosion: Eliminates $2^N$ subclass hierarchies by replacing them with $N$ composable decorators.
- Runtime Flexibility: Add, remove, or reorder responsibilities dynamically at runtime.
- Single Responsibility Principle: Isolates cross-cutting concerns (logging, metrics, caching) into dedicated classes.
Disadvantages
- Many Small Objects: Leads to systems with hundreds of tiny wrapping objects that are hard to inspect in stack traces.
- Order Dependency: Fragile behavior when specific decorators strictly require a precise wrapping order.
- Identity Discrepancy: The decorator is not reference-equal (
===) to the wrapped object, breaking code that relies on object identity.
When not to use it
Do not use Decorator when the component interface is enormous (e.g. 30+ methods). If Component has 30 methods, the BaseDecorator must maintain forwarding boilerplate for all 30 methods, increasing maintenance burden. In those scenarios, look into Proxy or AOP interceptors.
Related patterns
| Pattern | Interface Contract | Primary Focus |
|---|---|---|
| Decorator | Identical interface | Enhances responsibilities dynamically |
| Adapter | Different interface | Translates incompatible signatures |
| Proxy | Identical interface | Controls access, caching, or lazy loading |
| Strategy | Different interface (internal) | Changes the object's inner algorithm ("guts" vs "skin") |
| Composite | Identical interface | Organizes objects into tree hierarchies (many children) |
Decorator: data source pipeline
Refactor a monolithic data source that relies on flag arguments into composable Encryption and Compression decorators that stack in any order.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
How does Decorator differ from Proxy when both implement the same interface? Decorator dynamically enhances the target's behavior with new responsibilities (e.g., encryption, compression). Proxy controls access, lifecycle, or execution characteristics (e.g., security check, lazy loading, caching) without adding functional features.
Why does Java I/O rely so heavily on the Decorator pattern?
java.io avoids creating classes like BufferedGzipFileInputStream. By making InputStream the common component, any stream source can be combined with any processing filter (buffering, decompression, data parsing).
What is the downside of object identity in the Decorator pattern?
A decorator wraps a component, so decorator === component evaluates to false. Code that compares object references or stores components in Identity maps can exhibit subtle bugs when passed a decorated instance.
How can you remove a specific decorator from an active decorator stack? Removing an inner decorator requires traversing the chain, unwrapping the parent decorator's pointer, and re-pointing it to the inner child (similar to deleting a node in a linked list). Because this violates encapsulation, decorators are usually immutable once assembled.
Try it
Logging Framework LLD
Compose structured logging, redaction and bounded asynchronous output.