Explore the Decorator Pattern in Scala, leveraging wrappers, traits, and middleware-style composition to layer behavior around an existing component.
Decorator pattern: A structural pattern that wraps an existing component and adds behavior around it while preserving the same outward interface.
Decorator is common in Scala, but not always under that name. Middleware stacks, effect wrappers, HTTP clients with retries, logging layers, and metrics instrumentation are all decorator-like structures. The key idea is simple: preserve the contract, add cross-cutting behavior around it.
Decorator is useful when the original component should remain unchanged but callers need additional behavior such as:
That is better than editing the original type if the added behavior is optional, environment-specific, or orthogonal to the core responsibility.
1trait CurrencyService {
2 def rate(from: String, to: String): Either[String, BigDecimal]
3}
4
5final class LoggingCurrencyService(underlying: CurrencyService) extends CurrencyService {
6 override def rate(from: String, to: String): Either[String, BigDecimal] = {
7 val result = underlying.rate(from, to)
8 println(s"rate lookup: $from->$to result=$result")
9 result
10 }
11}
The logging wrapper does not change the contract. It adds behavior at the edge.
Classic examples use inheritance diagrams, but Scala code more often expresses decorator as wrapper composition:
This style fits Scala well because composition is usually clearer than subclass chains.
Traits can model decorator-like behavior, especially for reusable capability fragments. But deep mixin stacks can become harder to reason about than explicit wrappers. The main question is visibility:
Explicit constructor-based wrappers often answer those questions better than heavy trait stacking.
A good decorator preserves the essential expectations of the wrapped component. It may enrich timing, logging, or policy, but it should not silently change:
If behavior changes fundamentally, you may be building an adapter, facade, or entirely new abstraction instead.
Decorator composition is not automatically commutative. For example:
This is one reason Scala decorator stacks should be composed intentionally, not casually.
If wrappers can be combined in many orders, the code should make the intended order explicit.
Decorator is strongest for cross-cutting or boundary concerns. Core domain decisions usually deserve their own first-class place in the design.
Both are valid tools, but combining them freely can make call flow hard to understand.
Prefer explicit wrappers for runtime concerns such as logging, retries, metrics, and policy enforcement. Use traits when capability reuse is genuinely clearer, but make ordering and preserved contracts obvious. In Scala, the strongest decorator designs usually read like deliberate boundary composition rather than clever inheritance.