Symptom

One concrete class needs additional behaviour (logging, retries, encryption, caching, notification channels) that is genuinely orthogonal to its core responsibility. Subclassing for each combination produces a combinatorial hierarchy (Email + Sms + Slack + Logging + Retrying = 2^N classes); feature flags inside one class produce a Large Class that changes for unrelated reasons (Divergent Change).

Goal

Each cross-cutting feature is a separate wrapper class that conforms to the same interface as the wrappee. Features compose at the call site by nesting; the core class stays focused on its one responsibility.

Before the pattern

class EmailNotifier {
send(message) { /* email */ }
}
class EmailAndSmsNotifier {
send(message) { /* email + sms */ }
}
class EmailAndSlackNotifier {
send(message) { /* email + slack */ }
}
class EmailAndSmsAndSlackNotifier {
send(message) { /* email + sms + slack */ }
}
// N channels → 2^N notifier classes. Adding 'Discord' doubles the
// hierarchy. Each new combination repeats the orchestration logic.

After the pattern

class EmailNotifier {
send(message) { /* email */ }
}
class SmsDecorator {
constructor(wrapped) { this.wrapped = wrapped; }
send(message) {
this.wrapped.send(message);
/* also send sms */
}
}
class SlackDecorator {
constructor(wrapped) { this.wrapped = wrapped; }
send(message) {
this.wrapped.send(message);
/* also post to slack */
}
}
// Compose at the call site:
const notifier = new SlackDecorator(new SmsDecorator(new EmailNotifier()));
notifier.send('Build failed');
// N channels → N classes. Adding 'Discord' = 1 new decorator.
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 4. The book's running example is a windowing toolkit with scroll-bars and borders as decorators; this JavaScript adaptation uses a multi-channel notifier because the composability of channels makes the 2^N → N collapse visible in code.
Pressure

2^N subclasses or a 1-class-with-N-flags monolith — both options scale badly. Combining features at the construction site (with flags or factory methods) duplicates the orchestration logic; feature interactions become emergent and untestable in isolation.

Tradeoff

Reading code that uses Decorator requires navigating the wrapping chain. `new SlackDecorator(new SmsDecorator(new EmailNotifier()))` is a Russian doll the reader must unpack to know what 'send' actually does. Stack traces are deep; debugging requires understanding the wrapping order.

Relief

Each decorator is small, single-responsibility, unit-testable in isolation. New features = new wrappers, no edits to existing classes. Feature combinations are explicit at the construction site, where they belong; the type system enforces that every decorator conforms to the wrappee's interface.

Trap

Decorators that look up state on the wrappee or mutate the wrappee's behaviour stop being orthogonal additions and become semantic patches. Stack-of-decorators where order matters (RetryDecorator outside LoggingDecorator vs inside) and the ordering is not documented produces bugs no test exercises until production. Order-dependence demands explicit conventions or composition tools.