Decorator becomes useful when the base behavior is stable, but the way you wrap it keeps changing. That usually happens in systems where pricing, logging, validation, or policy layers need to stack without creating a new class for every combination.
Problem 1: Compose Optional Pricing Layers Without Class Explosion
Problem description: Pricing starts simple, then accumulates extra responsibilities such as:
- coupon discount
- tax
- metrics logging
At first, people often just add more conditionals into one calculator. Then the order starts to matter. Then observability shows up. Then someone wants one flow with coupon plus tax, another with tax only, and another with metrics around everything.
That is where a plain inheritance hierarchy starts getting silly.
What we are solving actually: We are solving for composable layering. The business wants some combinations of pricing behavior but not every combination hardcoded into a new subclass. The order of layers can also change the meaning, which makes composition explicit and important.
What we are doing actually:
- Keep one stable
PriceCalculatorcontract. - Start from a base calculator.
- Wrap it with optional decorators for discounting, tax, and observability.
- Make assembly order explicit at composition time.
Why Decorator Fits This Better Than Subclasses
The real problem is not “how do I add code around a method.” It is “how do I keep optional behavior composable without turning the model into a subclass matrix.”
Without Decorator, you quickly drift toward names like:
DiscountedPriceCalculatorDiscountedTaxedPriceCalculatorDiscountedTaxedMetricsPriceCalculator
That is not domain modeling. That is class explosion.
Decorator gives you a better shape:
- one stable contract
- one core implementation
- optional wrappers with explicit ordering
Structure
classDiagram
class PriceCalculator {
<<interface>>
+calculate()
}
class BasePriceCalculator
class PriceCalculatorDecorator
class CouponDecorator
class TaxDecorator
class MetricsDecorator
PriceCalculator <|.. BasePriceCalculator
PriceCalculator <|.. PriceCalculatorDecorator
PriceCalculatorDecorator <|-- CouponDecorator
PriceCalculatorDecorator <|-- TaxDecorator
PriceCalculatorDecorator <|-- MetricsDecorator
A Minimal Implementation
public interface PriceCalculator {
double calculate(Cart cart);
}
public final class BasePriceCalculator implements PriceCalculator {
@Override
public double calculate(Cart cart) {
return cart.getItems().stream().mapToDouble(Item::getPrice).sum();
}
}
public abstract class PriceCalculatorDecorator implements PriceCalculator {
protected final PriceCalculator delegate;
protected PriceCalculatorDecorator(PriceCalculator delegate) {
this.delegate = delegate;
}
}
public final class CouponDecorator extends PriceCalculatorDecorator {
public CouponDecorator(PriceCalculator delegate) {
super(delegate);
}
@Override
public double calculate(Cart cart) {
return delegate.calculate(cart) * 0.90; // Wrap the delegate result with coupon logic.
}
}
public final class TaxDecorator extends PriceCalculatorDecorator {
public TaxDecorator(PriceCalculator delegate) {
super(delegate);
}
@Override
public double calculate(Cart cart) {
return delegate.calculate(cart) * 1.18;
}
}
public final class MetricsDecorator extends PriceCalculatorDecorator {
public MetricsDecorator(PriceCalculator delegate) {
super(delegate);
}
@Override
public double calculate(Cart cart) {
long start = System.nanoTime();
double result = delegate.calculate(cart);
long duration = System.nanoTime() - start;
System.out.println("pricing.duration.nanos=" + duration);
return result;
}
}
Usage:
PriceCalculator calculator = new MetricsDecorator(
new TaxDecorator(
new CouponDecorator(
new BasePriceCalculator()
)
)
);
The important thing here is not that decorators can stack. Everyone already knows that part.
The important thing is that ordering carries business meaning. A coupon applied before tax can produce a different outcome than a coupon applied after tax. So the assembly code is not just plumbing. It is part of the pricing policy.
What Makes Decorator Dangerous
Decorator looks elegant in diagrams and messy in production if you overuse it.
Typical failure modes:
- the wrapping order is implicit and nobody knows which layer runs first
- too many small decorators make debugging painful
- domain behavior and technical behavior get mixed without boundaries
My bias is to use Decorator when the layers are genuinely orthogonal and when order can be made explicit in one place. If both of those are not true, simpler composition is usually better.
When I Would Still Choose Something Else
If the behavior is fixed and not optional, a regular service class is usually enough. If the object graph is getting hard to read, a composition factory or policy object is often a better boundary than ten decorators chained together.
Decorator is strongest when variability is real, the contract is stable, and the layering itself expresses useful intent.
Debug Steps
Debug steps:
- test the same cart with different decorator orderings to confirm business impact is understood
- log the assembled chain in one place so runtime layering is visible
- keep technical decorators and domain decorators conceptually separate
- simplify if the chain becomes so deep that debugging one request is painful
Categories
Tags
Comments