Somewhere in checkout, a conditional picks between three pricing formulas, and the formulas, the picking, and the code that uses the answer all live tangled in one function. Strategy is the Gang of Four behavioral pattern whose stated intent is to define a family of algorithms, encapsulate each one, and make them interchangeable, letting the algorithm vary independently from the clients that use it.
A strategy is really just an algorithm handed in as a value. The context runs whatever it's holding, the client decides what it holds, and the formulas become named, swappable, individually testable things instead of branches elbowing each other inside a function that grows with every business deal.
Here's the conditional version of a shipping calculator: flat rate, by weight, and express, three unrelated formulas sharing a function and a mode flag that every caller has to thread through. For an 8 kilogram package the three answers are worth computing by hand before checking the comments:
They print 10, 16, and 23. The function works, and its growth pattern is the problem: every new carrier deal lands as another branch in the same crowded body, testing any one formula means routing through the flag, and the string "express" travels through every layer of the call stack as an unchecked contract. The formulas have no names, no homes, and no independence.
The strategy version gives each formula a class honoring one signature, and a Checkout context that runs whichever it holds. The client constructs with flat rate, then swaps to by-weight, then express, calling the identical total(8) each time. Predict the three outputs and notice which class never changes:
The same 10, 16, 23, from one unchanged total() running three different algorithms. Each formula now has a name, its own file if it wants one, and a one-line unit test. A new carrier deal is a new class that touches nothing existing, and the comment in set_strategy() flags the detail that will matter shortly: the client makes every swap, never the strategies themselves.
Three roles. The Strategy interface (ShippingStrategy) fixes the signature the whole family shares. ConcreteStrategies implement one algorithm each, independent of and ignorant about their siblings. The Context (Checkout) holds one strategy and delegates, and the Client, the role that's easy to forget, picks which strategy the context holds and when that changes.
The pattern works like the travel-mode buttons on a maps app: driving, walking, and transit are three routing algorithms behind one "get directions" button, and the map renders whichever engine you picked without the engines ever switching themselves. The analogy carries the pattern's honest fine print, because somebody still does the picking. In the app it's your thumb, and in code it's selection logic that lives somewhere, often a small factory mapping mode names to strategies. The conditional doesn't vanish, it moves to one home and out of every algorithm user's way, and that relocation is the actual win.
Installing the pattern is four moves:
FlatRate, ByWeight, Express, holding one formula and nothing else. Small, named, and testable in isolation, which the branches never were.cost(weight_kg) in, integer out. The shared signature is what makes them interchangeable, and anything a variant needs beyond it travels in that variant's own constructor.Checkout receives its strategy and runs whatever it holds, with total() reduced to one delegation. The context computes nothing itself, which is why it never changes when the algorithm roster does.You might think this page and State describe one pattern, because the class diagrams are the same picture: a context holding an interface, concrete classes behind it, a setter for swapping. Whose hand is on the setter is the entire difference. Here, the client called set_strategy() all three times while the strategies stayed oblivious to each other's existence. In State, the state objects call the setter themselves, appointing their own successors, and the client just feeds events to a machine that steers itself.
The grep test from the State page works in both directions: find the setter's callers, and client-side calls mean Strategy while calls from inside the concrete classes mean State. The other cousin worth a sentence is Template Method, which also varies an algorithm but does it through inheritance at compile time, subclasses overriding steps of a fixed skeleton, where Strategy swaps whole algorithms through composition at runtime.
Strategy is the pattern that first-class functions partially dissolved, a point API designers have been making since lambdas reached Java: a one-method interface is a function type in ceremonial dress. The canonical sighting makes the case by itself. java.util.Comparator is a strategy for ordering, Collections.sort(list, comparator) is a context accepting one, and since lambdas arrived nobody writes a Comparator class when (a, b) -> a.age - b.age does it inline.
The Go and Rust tabs take the costume off completely. Go's strategy is a named function type, func(int) int, with plain functions as the variants, the same shape sort.Slice uses for its comparison argument. Rust stores a Box<dyn Fn> and takes closures, the shape of every sort_by call you've written. The class diagram survives where strategies carry configuration or several methods, and everywhere else the pattern lives on as its own ghost: a function parameter.
The pattern is an interface and a field, and these five keep the family honest:
Two questions before you go. First: the checkout printed 10, 16, and 23 from one unchanged method. Who made each algorithm choice, at what moment, and what would it mean for the pattern's identity if the strategies had made it instead? Second: the mode conditional didn't disappear from the program. Where did it go, and why is that still a win? Both answers are on this page.
Then go spot it in the wild, where this pattern hides inside arguments. Every comparator you pass to a sort, every retry policy handed to an HTTP client, and every pricing rule a checkout engine loads by plan tier is an algorithm injected from outside. Last, find a function in your own codebase that takes a mode flag and switches on it. Each branch is a strategy whose name the flag is hiding, and the refactor on this page is the act of letting them introduce themselves.
def calculate_shipping(mode: str, weight_kg: int) -> int:
# Three unrelated formulas sharing one function, and every
# new carrier deal lands as another elif right here
if mode == "flat":
return 10
elif mode == "weight":
return 2 * weight_kg
elif mode == "express":
return 15 + weight_kg
raise ValueError("unknown mode: " + mode)
def checkout_total(mode: str, weight_kg: int) -> int:
# The caller threads the mode flag through, knowing too much
return calculate_shipping(mode, weight_kg)
# Example usage
print(checkout_total("flat", 8)) # 10
print(checkout_total("weight", 8)) # 16
print(checkout_total("express", 8)) # 23from abc import ABC, abstractmethod
class ShippingStrategy(ABC): # Strategy: one algorithm family, one signature
@abstractmethod
def cost(self, weight_kg: int) -> int: ...
class FlatRate(ShippingStrategy): # ConcreteStrategy
def cost(self, weight_kg: int) -> int:
return 10
class ByWeight(ShippingStrategy): # ConcreteStrategy
def cost(self, weight_kg: int) -> int:
return 2 * weight_kg
class Express(ShippingStrategy): # ConcreteStrategy
def cost(self, weight_kg: int) -> int:
return 15 + weight_kg
class Checkout: # Context: runs whatever algorithm it's holding
def __init__(self, strategy: ShippingStrategy):
self._strategy = strategy
def set_strategy(self, strategy: ShippingStrategy) -> None:
self._strategy = strategy # the CLIENT calls this, never the strategies
def total(self, weight_kg: int) -> int:
return self._strategy.cost(weight_kg)
# Example usage: the client swaps algorithms, total() never changes
checkout = Checkout(FlatRate())
print(checkout.total(8)) # 10
checkout.set_strategy(ByWeight())
print(checkout.total(8)) # 16
checkout.set_strategy(Express())
print(checkout.total(8)) # 23