Java

Decorator Pattern in Java with Pricing and Observability Layers

4 min read Updated Mar 27, 2026

Java Design Patterns Series

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:

  1. Keep one stable PriceCalculator contract.
  2. Start from a base calculator.
  3. Wrap it with optional decorators for discounting, tax, and observability.
  4. 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:

  • DiscountedPriceCalculator
  • DiscountedTaxedPriceCalculator
  • DiscountedTaxedMetricsPriceCalculator

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

Continue reading

Previous Adapter Pattern in Java with a Third-Party Payment Gateway Example Next Facade Pattern in Java with Checkout Orchestration

Comments