Strategy Pattern

Encapsulate interchangeable algorithms behind a common interface. The dual of the Open-Closed Principle in practice.

Pattern Foundational
7 min read
pattern behavioral strategy algorithms composition

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 Sorter interface.
  • 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.00

Four details earn their keep:

  • No branching in Checkout. Adding BlackFridayDoubleDiscount later does not edit Checkout — it just implements PricingStrategy. That is the OCP in lived form.
  • Strategies are stateless or own their own state. PercentageDiscount holds its rate. The context does not know or care.
  • Validation in the strategy’s constructor. A bad percent fails at construction, not at finalPrice call 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#

VariantMechanismWhen it fits
Class-basedEach strategy is a separate class implementing the interface.The strategy holds state, has multiple methods, or needs construction parameters.
Lambda / function referenceThe 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-basedEach 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) — the Comparator is the strategy. Different comparators produce different sort orders without editing sort.
  • java.util.zip and java.util.compressDeflater, GZIPOutputStream, and friends are interchangeable compression strategies behind OutputStream.
  • 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 AIMovementStrategy for 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 PercentageDiscount does not need a Checkout.
  • 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 if branches. 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 CompositeDiscount that 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.

  • 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.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.