On this page
Description
Strategy is the behavioral pattern for interchangeable ways of doing one job: each variant of an algorithm - a pricing rule, a shipping calculation, a sort order - lives behind a shared interface, and the code that needs the result just calls whichever one it was given. You already use it every time you pass a Comparator or a key= function. This skill covers the pattern, how it replaces if chains, and where the "which strategy?" decision should live.
Shipping cost depends on the carrier the customer picks:
Java
double shippingCost(Order order, String carrier) {
if (carrier.equals("STANDARD")) return 5 + 1.5 * order.weightKg();
else if (carrier.equals("EXPRESS")) return 15 + 3 * order.weightKg();
else if (carrier.equals("PICKUP")) return 0;
// a new carrier means editing this method... again
throw new IllegalArgumentException(carrier);
}
Python
def shipping_cost(order: Order, carrier: str) -> float:
if carrier == "STANDARD":
return 5 + 1.5 * order.weight_kg
elif carrier == "EXPRESS":
return 15 + 3 * order.weight_kg
elif carrier == "PICKUP":
return 0
# a new carrier means editing this function... again
raise ValueError(carrier)
Every new carrier edits the same method, the branches can't be tested in isolation, and the same if chain tends to get copied wherever carriers matter.
We'll go from simple to more technical:
- The pattern: a strategy interface, its implementations, and the context that uses one.
- Applying it, step by step - and strategies as lambdas and functions.
- Where you'll see it: comparators, pricing rules, password encoders.
- Choosing the strategy, sharing strategies safely, and Strategy versus State.
The pattern
Strategy puts each variant of an algorithm in its own class behind a shared interface. The code that needs the result (the context) holds a strategy and calls it, without knowing which one it has.
Java
interface ShippingStrategy {
double cost(Order order);
}
class StandardShipping implements ShippingStrategy {
public double cost(Order o) { return 5 + 1.5 * o.weightKg(); }
}
class ExpressShipping implements ShippingStrategy {
public double cost(Order o) { return 15 + 3 * o.weightKg(); }
}
class Checkout {
private ShippingStrategy shipping;
void setShipping(ShippingStrategy s) { this.shipping = s; } // swap at runtime
double total(Order o) { return o.itemsTotal() + shipping.cost(o); }
}
Python
from abc import ABC, abstractmethod
class ShippingStrategy(ABC):
@abstractmethod
def cost(self, order: Order) -> float: ...
class StandardShipping(ShippingStrategy):
def cost(self, o: Order) -> float:
return 5 + 1.5 * o.weight_kg
class ExpressShipping(ShippingStrategy):
def cost(self, o: Order) -> float:
return 15 + 3 * o.weight_kg
class Checkout:
def __init__(self, shipping: ShippingStrategy):
self.shipping = shipping # swap at runtime by assigning another
def total(self, o: Order) -> float:
return o.items_total + self.shipping.cost(o)
In modern Java a strategy interface with one method can also be a lambda: checkout.setShipping(o -> 0.0); for store pickup. In Python that's the usual form: a one-method strategy is just a function, Callable[[Order], float] - checkout.shipping = lambda o: 0.0 - and a registry is a dict from carrier name to function.
How it works, step by step
- Identify the part that varies (how shipping is priced).
- Put it behind an interface with one clear method.
- Write one small class (or lambda) per variant.
- The context receives a strategy - through its constructor, a setter, or a lookup map - and just calls it.
- A new variant is a new class; nothing else changes.
Where you'll see it
Comparatoris the everyday example:list.sort(Comparator.comparing(Employee::salary))passes a sorting strategy. In Python it's thekeyfunction:employees.sort(key=attrgetter("salary")).- Discount and pricing rules, tax calculations per country, retry/backoff policies, compression or encryption algorithms.
- Spring picks between strategies constantly - for example different
PasswordEncoderimplementations behind one interface.
Choosing among the behavioral patterns
| If you need to... | Reach for | It's about |
|---|---|---|
| Let many objects react when something happens, without the source knowing them | Observer | Who gets told about a change. |
| Swap one algorithm for another at runtime | Strategy (this lesson) | How a task is done - chosen from outside. |
| Turn a request into an object you can queue, log, undo or redo | Command | What to do, packaged for later. |
| Make an object behave differently depending on its current state | State | Behavior that changes itself as the object moves between states. |
| Fix the steps of an algorithm but let subclasses fill in some of them | Template Method | The skeleton stays, the details vary. |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
The if chain just moves to every place that picks a strategy | the same edit in many places for each new variant | choose in one place - a registry map |
| A strategy interface for two variants that never change | indirection for nothing | a plain if |
| Strategies needing very different inputs through one interface | awkward, growing parameter lists | pass a context or parameter object |
| Mutable state inside a shared strategy | races between requests | keep shared strategies stateless |
| Strategies that replace themselves as events arrive | a hidden state machine | use the State pattern |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. After refactoring to Strategy, where should the "which strategy?" decision live?
Show answer
In one place - typically a map from key (carrier name, campaign type) to strategy, built once at startup - not in if chains copied to every caller.
2. Your "strategies" start replacing themselves as events arrive. What are you really building?
Show answer
A state machine. With Strategy the client picks the algorithm; when the object switches its own behavior in response to events, model it with the State pattern.
3. You have exactly two variants that will never change. Do they need a strategy interface?
Show answer
No. A simple if is clearer; Strategy pays off when variants keep being added or must be swapped and tested independently.
Key takeaways
- Each variant of an algorithm lives in its own class (or lambda) behind one interface.
- The context calls the strategy without knowing which one it has, and it can be swapped at runtime.
- New variants are new classes - existing code stays closed for modification.
- Keep the "which strategy?" decision in one place, such as a registry map.
Comparatoris Strategy you already use every day (in Python, thekey=function you pass tosorted).