Code that picks a different algorithm by condition tends to grow into if/switch branches. One branch per payment method, per sort order, per compression scheme. Every new algorithm means opening and editing that branch. Strategy encapsulates each algorithm as an object so they can be swapped.

Shared Intent

Strategy has three intents.

  • Encapsulate the algorithm — each algorithm sits behind the same interface, forming one interchangeable family.
  • Swap at runtime — the context is injected with which strategy to use at runtime. Behavior changes without editing code.
  • Extend by adding — a new algorithm enters as a new strategy, not as an edit to a branch. Existing code stays untouched.

The GoF Form

GoF defines three roles: Context, Strategy, ConcreteStrategy.

The Context holds a strategy by reference and delegates execution to it. Strategy is the common interface for the algorithm, and each ConcreteStrategy implements one algorithm. The Context calls only through the interface, unaware of which ConcreteStrategy it received.

flowchart LR
    Context --> S["Strategy"]
    S --> A["RateDiscount"]
    S --> B["AmountDiscount"]
    S --> C["NoDiscount"]
interface DiscountStrategy {
    int apply(int price);
}

class RateDiscount implements DiscountStrategy {
    public int apply(int price) { return price * 9 / 10; }
}

class Checkout {
    private final DiscountStrategy discount;
    Checkout(DiscountStrategy discount) { this.discount = discount; }
    int total(int price) { return discount.apply(price); }
}

You inject the strategy, as in new Checkout(new RateDiscount()). When a new discount appears, you add one class implementing DiscountStrategy and leave Checkout alone.

Language Implementations

Java’s Comparator is the representative Strategy. In list.sort(comparator) the sorting algorithm is fixed and only the comparison rule is injected as a strategy. A new sort order is expressed as a new Comparator.

Python simply passes the strategy as a function. With first-class functions, no separate Strategy class is needed.

from typing import Callable

def checkout(price: int, discount: Callable[[int], int]) -> int:
    return discount(price)

checkout(10000, lambda p: p * 9 // 10)

sorted(data, key=func) has the same shape: the comparison strategy is passed as a function. A single function plays the ConcreteStrategy role instead of a class hierarchy.

TypeScript expresses it with a function type too. Accepting a type like (price: number) => number lets you inject the strategy as a function. Interfaces and classes also work, but the function form is more concise.

Distinguishing from State

Strategy and State share almost the same structure: a context delegating to an interface. But their intents differ.

  • Strategy — the outside chooses the algorithm. The strategies don’t know one another.
  • State — a state transitions to the next state on its own. The states know the transition relationships.

They use the same delegation structure, but who decides the swap is different.

When Language Features Absorb It

The heart of Strategy is “inject an interchangeable behavior.” In languages with first-class functions, that behavior is expressed as a single function, so the separate Strategy interface and ConcreteStrategy class hierarchy disappear.

strategies = {
    "rate": lambda p: p * 9 // 10,
    "amount": lambda p: p - 2000,
}
discount = strategies["rate"]

Three classes shrink to three functions. Just as Builder is absorbed into named/default parameters, Strategy is absorbed into first-class functions. The pattern doesn’t vanish; the problem it solved is handled at the language level.

Conclusion

Strategy encapsulates a family of algorithms behind a single interface and swaps them at runtime. It extends by adding a strategy rather than editing a branch. The choice comes down to two questions.

  • Is there a different algorithm per condition, swapped at runtime? Then Strategy over branching.
  • Does your language have first-class functions? Then the Strategy class hierarchy collapses into function injection.

Comparator, sorted(key=...), and function injection are all variations on the same intent. What separates it from the structurally similar State is who decides the swap.

References