Strategy Pattern
Encapsulate interchangeable algorithms behind a common interface. The dual of the Open-Closed Principle in practice.
What it is#
The Strategy pattern defines a family of algorithms, encapsulates each one as an object, and makes them interchangeable at runtime. The client holds a reference to a Strategy interface; the concrete strategy plugged in decides how the operation is carried out.
If Observer is one-to-many notification, Strategy is one-to-one delegation. The context object owns a reference to a strategy, calls it when the operation is needed, and never branches on which strategy is in place. New algorithms become new classes — not new if arms inside the context. That is the Open-Closed Principle stated as a structure.
Class structure#
┌────────────────────┐ ┌──────────────────────┐ │ Context │◇───────►│ Strategy │ ├────────────────────┤ 1 ├──────────────────────┤ │ strategy │ │ execute(input) │ │ setStrategy(s) │ └──────────┬───────────┘ │ doWork(input) │ │ └────────────────────┘ │ ┌───────────────┼───────────────┐ │ │ │ ┌──────────┴───┐ ┌─────────┴────┐ ┌────────┴─────┐ │ StrategyA │ │ StrategyB │ │ StrategyC │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ execute(in) │ │ execute(in) │ │ execute(in) │ └──────────────┘ └──────────────┘ └──────────────┘The diamond is composition with delegation — Context holds one Strategy at a time but the choice is mutable.
When to use it#
Reach for Strategy when every one of these holds:
- A single operation has multiple valid implementations (sort, compress, price, route, validate).
- The choice of implementation depends on runtime data — user preference, configuration, request shape, feature flag.
- The implementations vary independently and are likely to grow in number over time.
- The context should not need to know which implementation is plugged in — it just calls
execute.
Common scenarios:
- Sorting — quick-sort, merge-sort, radix-sort behind one
Sorterinterface. - Pricing — regular price, member price, holiday surcharge, dynamic pricing all behind one
PricingStrategy. - Compression — gzip, brotli, zstd selected per request.
- Payment routing — Stripe, Razorpay, PayPal behind one
PaymentProcessor. - Authentication — password, OTP, biometric, SSO behind one
AuthStrategy.
When not to use it:
- When there is exactly one implementation and the second is genuinely hypothetical. Adding the interface ahead of demand creates speculative complexity. Wait for the second algorithm to exist.
- When the algorithms differ in signature, not just behavior. If one needs three inputs and another needs one, they are not the same strategy — they are different operations.
- When
if (type == X)covers two cases that will never grow. The compile-time conditional is simpler than a class hierarchy.
How it works#
A canonical example: an order checkout that supports multiple discount algorithms. New discount rules become new strategy classes, not new branches in Checkout.
import java.math.BigDecimal;import java.math.RoundingMode;
public interface PricingStrategy { BigDecimal apply(BigDecimal subtotal);}
public final class NoDiscount implements PricingStrategy { @Override public BigDecimal apply(BigDecimal subtotal) { return subtotal; }}
public final class PercentageDiscount implements PricingStrategy { private final BigDecimal percent; public PercentageDiscount(BigDecimal percent) { if (percent.signum() < 0 || percent.compareTo(BigDecimal.valueOf(100)) > 0) throw new IllegalArgumentException("percent must be in [0, 100]"); this.percent = percent; } @Override public BigDecimal apply(BigDecimal subtotal) { BigDecimal multiplier = BigDecimal.ONE.subtract(percent.divide(BigDecimal.valueOf(100))); return subtotal.multiply(multiplier).setScale(2, RoundingMode.HALF_UP); }}
public final class FlatAmountOff implements PricingStrategy { private final BigDecimal amount; public FlatAmountOff(BigDecimal amount) { this.amount = amount; } @Override public BigDecimal apply(BigDecimal subtotal) { BigDecimal result = subtotal.subtract(amount); return result.signum() < 0 ? BigDecimal.ZERO : result.setScale(2, RoundingMode.HALF_UP); }}
public final class Checkout { private PricingStrategy pricing;
public Checkout(PricingStrategy initial) { this.pricing = initial; }
public void setPricing(PricingStrategy s) { this.pricing = s; }
public BigDecimal finalPrice(BigDecimal subtotal) { return pricing.apply(subtotal); }}
// Wiring:// Checkout c = new Checkout(new NoDiscount());// c.finalPrice(new BigDecimal("100.00")); // 100.00// c.setPricing(new PercentageDiscount(BigDecimal.TEN));// c.finalPrice(new BigDecimal("100.00")); // 90.00// c.setPricing(new FlatAmountOff(new BigDecimal("15.00")));// c.finalPrice(new BigDecimal("100.00")); // 85.00Four details earn their keep:
- No branching in
Checkout. AddingBlackFridayDoubleDiscountlater does not editCheckout— it just implementsPricingStrategy. That is the OCP in lived form. - Strategies are stateless or own their own state.
PercentageDiscountholds its rate. The context does not know or care. - Validation in the strategy’s constructor. A bad percent fails at construction, not at
finalPricecall time — closer to the source of the error. - The strategy returns a value. Strategy is synchronous and pull-shaped; Observer is asynchronous and push-shaped. Confusing the two is a frequent interview slip.
Variants#
| Variant | Mechanism | When it fits |
|---|---|---|
| Class-based | Each strategy is a separate class implementing the interface. | The strategy holds state, has multiple methods, or needs construction parameters. |
| Lambda / function reference | The strategy is a Function<T, R>, Comparator<T>, or Runnable. | The strategy is stateless and a single method — the class boilerplate is pure ceremony. |
| Enum-based | Each enum constant implements the strategy method. | The set of strategies is fixed, small, and known at compile time. enum gives you exhaustiveness checks for free. |
The JDK is full of strategy-shaped types: Comparator<T> for sort order, Runnable for “what to run”, Callable<V> for “what to run that returns a value”, Function<T, R> for generic mappings, Predicate<T> for filters. Treat any single-abstract-method interface as the lambda form of Strategy — they were renamed @FunctionalInterface in Java 8 but the pattern is older.
Example systems#
The pattern is everywhere; you have used it without naming it:
Collections.sort(list, comparator)— theComparatoris the strategy. Different comparators produce different sort orders without editingsort.java.util.zipandjava.util.compress—Deflater,GZIPOutputStream, and friends are interchangeable compression strategies behindOutputStream.- Spring’s
AuthenticationProvider— username/password, OAuth2, JWT, LDAP all implement the same provider interface; the security filter selects one at runtime. - Routing tables — a router holds a
RoutingStrategy(shortest path, lowest latency, sticky-session) and swaps implementations without touching forwarding code. - A/B test frameworks — each variant is a strategy; an assignment service picks one per request.
- Game AI —
MovementStrategyfor an enemy: chase, flee, patrol, ambush. The enemy entity holds one and swaps based on game state.
Trade-offs#
What you gain:
- Open for extension, closed for modification. New algorithms are new classes. The context never changes — and neither do the tests for the context.
- Each strategy is independently testable. A unit test for
PercentageDiscountdoes not need aCheckout. - Runtime selection. The strategy can be chosen from config, feature flag, A/B assignment, or user preference — without recompilation.
- The Liskov Substitution Principle gets a fighting chance. Every strategy honors the same contract, so swapping one for another cannot break the context.
What you pay:
- More classes. Three discount types is three more files than three
ifbranches. For trivial logic that will never grow, this is overkill. - Strategy proliferation. “Just one more strategy” repeated fifty times produces a folder of nearly-identical classes. Refactor toward parameterized strategies or composition (a
CompositeDiscountthat chains others) before the directory becomes unbrowsable. - Strategy interface drift. If the context grows to need extra information (
apply(subtotal, customer, region)), every strategy must change. The interface is a coupling point; design it for the dimensions you expect to vary. - Client knows the strategies. Someone has to pick
new PercentageDiscount(10). Often a Factory or a configuration loader takes that responsibility — Strategy and Factory are common collaborators.
Strategy: composition + delegation
The context holds a strategy reference and calls it. The two are wired at runtime — the same Checkout can use any strategy.
Template Method: inheritance + hooks
The base class owns the algorithm skeleton and calls protected hooks. Subclasses fill in the variable steps. The wiring is at compile time.
Both vary an algorithm; they choose a different axis. Strategy wins when the variation is runtime and the algorithms have no common skeleton. Template Method wins when there is a fixed skeleton with small holes — see template-method-pattern.
Related patterns#
- Open Closed Principle (OCP) — Strategy is the structural answer to OCP for “we keep adding new ways to do this one thing.” The two are the same idea said in different vocabularies.
- State Pattern — same structure as Strategy (context holds a polymorphic collaborator), different intent. State swaps itself based on internal events; Strategy is swapped from the outside.
- Template Method Pattern — the inheritance-based cousin. Use when there is a real algorithm skeleton, not just an interchangeable operation.
- Observer Pattern — Observer is push and broadcast; Strategy is pull and one-to-one.
- Dependency Inversion Principle (DIP) — Strategy is DIP in miniature: the context depends on an abstraction, not on concrete algorithm classes.
- Factory Method Pattern — pairs naturally with Strategy when “which strategy” is itself a non-trivial decision.