The Decorator Pattern: Adding Behavior Without Touching Existing Code

2026-04-27

You have a working service class. Now you need to add logging. Then caching. Then metrics. Then retry logic. If you keep cramming all of that into the original class, you end up with a 500-line method where the actual business logic is buried under infrastructure concerns. The Decorator Pattern solves this by letting you wrap objects with new behavior while keeping the same interface.

The core idea: a decorator implements the same interface as the object it wraps, delegates the core work to that object, and adds its own behavior before or after. Each decorator is a thin, single-responsibility layer.

Real-world example: Suppose you have an OrderService with a placeOrder(order) method. Instead of modifying that class, you build decorators:

You compose them at the wiring layer:

service = RetryOrderService(LoggingOrderService(MetricsOrderService(RealOrderService())))

Each class is 15–30 lines. Each is independently testable. You can reorder them, remove one, or add a new one without touching any of the others. The RealOrderService never knows it's being decorated.

Where this beats inheritance: If you used subclassing, you'd face a combinatorial explosion. Logging + metrics? New subclass. Logging + retry? Another subclass. Logging + metrics + retry? Yet another. With n behaviors, inheritance gives you up to 2n - 1 subclasses. Decorators give you exactly n classes that compose freely. For 5 cross-cutting concerns, that's 31 subclasses vs. 5 decorators.

When to use it:

When to avoid it:

Practical tip: In languages with interfaces or protocols, define the contract explicitly. Every decorator and the real implementation should implement the same interface. This makes the compiler enforce correctness instead of relying on convention.

See it in action: Check out Use the Decorator Pattern To Reduce Code Duplication in Complex Models by Zoran on C# to see this theory applied.
Key Takeaway: The Decorator Pattern lets you layer behaviors like logging, caching, and retries onto existing objects without modifying them — keeping each concern in its own small, testable class that composes freely at runtime.

All newsletters