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).
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.
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.
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.
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.
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.