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).
Combinatorial subclass explosion or feature-flag-laden monoliths the agent must verify exhaustively on every behavioural change. 2^N combinations means N×(N-1) feature-interaction cells the agent reasons about; feature flags inside one class make the class's behaviour parametric on flag combinations.
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.
N small wrapper classes the agent reads one at a time. Feature composition is explicit in the construction expression; each wrapper's behaviour is its own one-file unit; tests cover each wrapper × wrappee combination compositionally.
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.
Combinatorial hierarchy growth turns 'add a feature' into 'edit every existing combination'. Feature-flag monoliths concentrate the verification burden into one mega-class whose behaviour the agent must trace through every flag-permutation. Both shapes consume context budget that should be spent on the calling code.
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.
Wrapping chains are stack-allocated indirection the agent must navigate per operation. Tracing why `notifier.send(msg)` produced a specific side effect requires the agent to walk the wrapping chain — and the chain's composition is dynamic, set at the construction site that may live in a different module.
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.
Per-feature edits scope to one decorator file; verification scales linearly with feature count. Stack traces map decorators 1:1 to call frames; the agent can localize bugs by reading one wrapper at a time. Compositional tests cover combinations without enumerating them.
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.
Decorators whose semantics depend on wrapping order (RetryDecorator(LoggingDecorator(X)) vs LoggingDecorator(RetryDecorator(X))) force the agent to verify order-correctness on every construction site. The construction expression becomes part of the spec; PR review must catch wrong-order regressions the type system cannot.