Strategy Pattern
1. What the strategy pattern is
The strategy pattern turns an algorithm into an object behind an interface, so the code that uses the algorithm can swap it at runtime without knowing which one it’s running. The caller holds a reference to “a strategy” and calls it; which concrete algorithm sits behind that reference is decided elsewhere.
The Gang of Four put it in the behavioural bucket because the load-bearing decision is a runtime one: which algorithm runs. A family of algorithms that all solve the same problem (compute a price, sort a list, compress a buffer) gets factored out of the class that needs them and into a set of interchangeable objects. The class that uses them depends only on the interface.
Define a family of algorithms, put each in its own class behind a common interface, and make them interchangeable.
That’s the whole pattern. The rest is mechanics, plus a question Java raises that GoF couldn’t: when the JDK already gives you functional interfaces, do you still need the classes?
2. The problem: branching on a type flag
The pattern earns its place when an object’s behaviour varies along one axis and the variation is currently expressed as a conditional. Take a checkout that computes shipping cost. The shipping method is a field, and the cost calculation branches on it:
public class Checkout {
public double shippingCost(Order order, String method) {
if (method.equals("standard")) {
return order.weight() * 0.5;
} else if (method.equals("express")) {
return order.weight() * 0.5 + 12.0;
} else if (method.equals("free")) {
return order.total() >= 50.0 ? 0.0 : order.weight() * 0.5;
}
throw new IllegalArgumentException("unknown method: " + method);
}
}This works, and for three cases it’s even readable. The trouble is what it does to every future change. Adding overnight shipping means editing Checkout itself, even though Checkout has nothing to do with overnight rates. That’s an open/closed violation: the class is open to modification for a reason that isn’t its own. Each branch is welded to the others too, so you can’t unit-test express pricing without constructing a Checkout and passing the right magic string. The String method flag is unchecked on top of that, so a typo ("expres") is a runtime exception, not a compile error.
The branches don’t share state or fall through to each other. They’re independent algorithms stapled into one method. The strategy pattern un-staples them.
3. Writing one in Java
Three roles. The strategy interface declares the algorithm’s signature, each concrete strategy is one implementation, and the context holds a strategy reference and delegates to it instead of branching.
The interface is the contract every shipping algorithm satisfies:
public interface ShippingStrategy {
double cost(Order order);
}Each branch from the cascade becomes its own class:
public class StandardShipping implements ShippingStrategy {
@Override
public double cost(Order order) {
return order.weight() * 0.5;
}
}
public class ExpressShipping implements ShippingStrategy {
@Override
public double cost(Order order) {
return order.weight() * 0.5 + 12.0;
}
}
public class FreeShipping implements ShippingStrategy {
@Override
public double cost(Order order) {
return order.total() >= 50.0 ? 0.0 : order.weight() * 0.5;
}
}The context holds a ShippingStrategy and delegates. It never branches on a type flag, and it never imports a concrete strategy:
public class Checkout {
private ShippingStrategy strategy;
public Checkout(ShippingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(ShippingStrategy strategy) {
this.strategy = strategy;
}
public double shippingCost(Order order) {
return strategy.cost(order);
}
}The caller picks the strategy and hands it in:
Checkout checkout = new Checkout(new ExpressShipping());
double cost = checkout.shippingCost(order);
checkout.setStrategy(new FreeShipping()); // swapped at runtimeCompare this to the cascade. Adding overnight shipping is a new class that implements ShippingStrategy; Checkout is not touched, which is the open/closed property the cascade lacked. ExpressShipping is testable on its own, with no Checkout around it. The strategy is a typed object too, so a wrong choice fails to compile instead of throwing at runtime. The price is visible: three branches of conditional became three classes plus an interface. Worth it when the algorithm family is genuinely open-ended, overkill when it isn’t — section 5.
4. Strategies as lambdas
A stateless strategy doesn’t need a class at all. ShippingStrategy declares exactly one abstract method, which makes it a functional interface, and any functional interface can be implemented by a lambda instead of a named class. Every concrete strategy above is stateless (a single method, no fields), so the three classes collapse into three expressions:
ShippingStrategy standard = order -> order.weight() * 0.5;
ShippingStrategy express = order -> order.weight() * 0.5 + 12.0;
ShippingStrategy free = order ->
order.total() >= 50.0 ? 0.0 : order.weight() * 0.5;
Checkout checkout = new Checkout(express);Checkout does not change at all. It still depends on ShippingStrategy, and a lambda is a ShippingStrategy. The strategy pattern, with the boilerplate deleted by the language. Marking the interface @FunctionalInterface is optional but worth it: it makes the compiler reject a second abstract method, so the interface’s lambda-compatibility becomes a checked contract rather than an accident.
The JDK leans on this so heavily that one of its core interfaces is the strategy pattern with the serial numbers filed off. Comparator<T> is a family of “how to order two elements” algorithms, made interchangeable behind one interface, passed into a context (Collections.sort, Stream.sorted, TreeMap) that delegates the comparison without knowing which ordering it got:
orders.sort(Comparator.comparingDouble(Order::total)); // by total
orders.sort(Comparator.comparingDouble(Order::weight)); // by weightA class still beats a lambda in a few cases. When the strategy carries state (a TaxStrategy configured with a regional rate), it needs fields, so it needs a class or at least a constructor. A long algorithm is the second case: a named class documents intent better than a multi-line lambda buried at a call site. Reuse is the third — a strategy used from many call sites wants one home, not a copy-pasted lambda. For a stateless one-liner used once, the lambda wins, and the class is ceremony with no payoff.
5. When to use it (and strategy vs state)
Reach for the strategy pattern when one behaviour varies independently of the object that uses it, the variants are a genuinely open set, and you’re currently expressing that variation as a conditional on a type flag. All three need to hold. If the set is closed and small, or the branches are two lines that will never grow, the if/else cascade is fine and the pattern is just indirection.
The pattern that gets confused with strategy is state, because both replace a conditional with a set of interchangeable objects behind an interface. The distinction is who swaps the object and why:
- Strategy is chosen from outside and usually doesn’t change. The caller picks
ExpressShippingand the choice reflects a decision the caller made. One strategy doesn’t know the others exist. - State swaps itself from inside. A
Connectionin itsOpenstate transitions toClosedas a consequence of its own behaviour, and each state knows which state comes next.
A rule of thumb: if the interchangeable objects need to know about each other to do their job, that’s state. If each one is self-contained and the choice comes from a caller, that’s strategy.
The lighter-weight question to ask first: do you even need a named pattern here? In modern Java, “this method takes a function and calls it” is often enough. A single ShippingStrategy parameter passed as a lambda gets you the runtime swap, the testability and the open/closed property without an interface hierarchy or a context class. The set of cases where the full pattern, with its named interface and concrete classes, earn its keep is small: strategies that carry state, strategies that are a documented part of the domain, strategies selected by name from a registry. Until then, a function parameter is the strategy pattern, and it’s all you need.