📅 Register (free): Spec-Driven Development with ATDD & AI (Oct 14)
The Strategy pattern is usually introduced with a diagram:
Context
└── Strategy
├── Strategy A
├── Strategy B
└── Strategy CUseful. But in a real application, the more important question is:
Where should those strategies live in Clean Architecture?
The problem
You’re placing an order, and the shipping cost depends on where it’s going.
public Money calculateShipping(Order order) {
if (order.country().zone() == ShippingZone.DOMESTIC) {
return Money.of(5);
}
return Money.of(15);
}That works. Then the rules grow:
public Money calculateShipping(Order order) {
var zone = order.country().zone();
if (zone == ShippingZone.DOMESTIC) {
return order.subtotal().isAtLeast(Money.of(50))
? Money.ZERO
: Money.of(5);
} else if (zone == ShippingZone.NORTH_AMERICA) {
return Money.of(10).add(Money.of(1).multiply(order.itemCount()));
} else if (zone == ShippingZone.INTERNATIONAL) {
return Money.of(20)
.add(Money.of(2).multiply(order.itemCount()))
.add(Money.of(15));
}
throw new IllegalStateException(”Unknown zone: “ + zone);
}Now the code that places the order knows every shipping rule in the system. Every new market means editing it. Every rate change means editing it.
Step 1: The domain defines the interface
Define what the business needs, not how it’s done:
public interface ShippingPolicy {
Money calculate(Money subtotal, Quantity quantity);
}This is something the business needs: “give me the shipping cost for an order of this value and size”. So the domain owns it.
Notice the name: ShippingPolicy, not ShippingStrategy. “Strategy” is the pattern’s name; “policy” is the business’s word for it. The interface is a Strategy, but you name it in the language of the domain.
Step 2: Each rule becomes a class
public final class DomesticShipping implements ShippingPolicy {
private static final Money FREE_SHIPPING_THRESHOLD = Money.of(50);
private static final Money FLAT_RATE = Money.of(5);
@Override
public Money calculate(Money subtotal, Quantity quantity) {
if (subtotal.isAtLeast(FREE_SHIPPING_THRESHOLD)) {
return Money.ZERO;
}
return FLAT_RATE;
}
}public final class NorthAmericaShipping implements ShippingPolicy {
private static final Money BASE = Money.of(10);
private static final Money PER_ITEM = Money.of(1);
@Override
public Money calculate(Money subtotal, Quantity quantity) {
return BASE.add(PER_ITEM.multiply(quantity.value()));
}
}public final class InternationalShipping implements ShippingPolicy {
private static final Money BASE = Money.of(20);
private static final Money PER_ITEM = Money.of(2);
private static final Money CUSTOMS_HANDLING = Money.of(15);
@Override
public Money calculate(Money subtotal, Quantity quantity) {
return BASE
.add(PER_ITEM.multiply(quantity.value()))
.add(CUSTOMS_HANDLING);
}
}Each rule can change without touching the others or the use case.

