Strategy Pattern
Strategy works by pulling one varying piece of behaviour out of a class and behind an interface, then storing a reference to a concrete implementation of that interface as a field — so the owning object calls field.execute(...) and the actual algorithm is decided by which object you assigned to the field, swappable at runtime without touching the caller.
A logistics firm must quote a shipping cost. The cost depends on the carrier mode — standard road, express air, international sea — and each mode prices differently: road is a flat base plus per-kg, air adds a fuel surcharge, sea is cheap per-kg but has a high fixed customs/handling floor. Cramming all three into one calculateCost() with a growing if (mode == ...) chain means every new carrier edits the same method, every pricing tweak risks the others, and the branch list becomes the bottleneck. Strategy makes each pricing rule a separate object implementing one ShippingStrategy interface; the ShippingService holds whichever one the order needs and just calls it.
Worked example — one order, three strategies, real numbers
Order: weight 20 kg, distance 1500 km, declared value $800. Each strategy implements cost(weightKg, distanceKm, declaredValue) with these concrete rules:
| Strategy | Formula | Computed for 20 kg / 1500 km / $800 | Result |
|---|---|---|---|
| StandardRoad | $5 base + $0.50/kg + $0.01/km | 5 + (0.50 × 20) + (0.01 × 1500) = 5 + 10 + 15 | $30.00 |
| ExpressAir | $15 base + $1.20/kg + 8% fuel surcharge on subtotal | (15 + 1.20 × 20) = 39; 39 × 1.08 | $42.12 |
| InternationalSea | $0.20/kg + 2% of declared value, floored at $25 customs minimum | (0.20 × 20) + (0.02 × 800) = 4 + 16 = 20 → below $25 floor | $25.00 |
The trace through the runtime call, after the order picks ExpressAir:
service.setStrategy(new ExpressAir())— the field now points at the air-pricing object. The service code did not change.service.quote(20, 1500, 800)runs; internally it doesreturn strategy.cost(20, 1500, 800)— it has no idea which formula it just invoked.ExpressAir.costcomputes 15 + 1.20×20 = 39, applies ×1.08, returns 42.12.- Customer upgrades to international. Caller does
service.setStrategy(new InternationalSea())and re-quotes — same line of service code, different object, returns 25.00 (the $20 raw cost was lifted to the $25 floor inside the strategy, where that rule belongs).
Implementation
Java — note the floor rule lives inside InternationalSea, not in the service:
interface ShippingStrategy {
double cost(double weightKg, double distanceKm, double declaredValue);
}
class StandardRoad implements ShippingStrategy {
public double cost(double w, double d, double v) {
return 5.0 + 0.50 * w + 0.01 * d;
}
}
class ExpressAir implements ShippingStrategy {
public double cost(double w, double d, double v) {
double subtotal = 15.0 + 1.20 * w;
return subtotal * 1.08; // 8% fuel surcharge
}
}
class InternationalSea implements ShippingStrategy {
public double cost(double w, double d, double v) {
double raw = 0.20 * w + 0.02 * v;
return Math.max(raw, 25.0); // $25 customs floor
}
}
class ShippingService {
private ShippingStrategy strategy;
void setStrategy(ShippingStrategy s) { this.strategy = s; }
double quote(double w, double d, double v) {
return strategy.cost(w, d, v); // no idea which formula this is
}
}
// usage
ShippingService svc = new ShippingService();
svc.setStrategy(new ExpressAir());
svc.quote(20, 1500, 800); // 42.12
svc.setStrategy(new InternationalSea());
svc.quote(20, 1500, 800); // 25.00Go — strategies are just function values, the lightest form of the pattern:
package shipping
import "math"
type Strategy func(weightKg, distanceKm, declaredValue float64) float64
func StandardRoad(w, d, v float64) float64 { return 5 + 0.50*w + 0.01*d }
func ExpressAir(w, d, v float64) float64 { return (15 + 1.20*w) * 1.08 }
func InternationalSea(w, d, v float64) float64 {
return math.Max(0.20*w+0.02*v, 25)
}
type Service struct{ strategy Strategy }
func (s *Service) SetStrategy(st Strategy) { s.strategy = st }
func (s *Service) Quote(w, d, v float64) float64 { return s.strategy(w, d, v) }
// usage
// svc := &Service{}
// svc.SetStrategy(ExpressAir); svc.Quote(20, 1500, 800) // 42.12
// svc.SetStrategy(InternationalSea); svc.Quote(20, 1500, 800) // 25.00Why the naive version is wrong
The tempting first cut puts the branching inside the service:
double quote(String mode, double w, double d, double v) {
if (mode.equals("road")) return 5 + 0.50*w + 0.01*d;
else if (mode.equals("air")) return (15 + 1.20*w) * 1.08;
else if (mode.equals("sea")) return Math.max(0.20*w + 0.02*v, 25);
throw new IllegalArgumentException(mode); // forgot a case? runtime blow-up
}Every new carrier edits this method (violates Open/Closed), the customs-floor rule and the fuel-surcharge rule sit tangled in one place, the rules can't be unit-tested in isolation, and an unknown mode string only fails at runtime instead of being a compile-time type. Strategy turns each branch into a separately testable, separately deployable object and lets the compiler enforce that a strategy was supplied.
Pitfalls
- Forgetting to set a strategy. A null/zero-value strategy field throws NPE (Java) or panics (Go nil func) the first time
quote()runs. Default to a sensible strategy in the constructor, or fail loudly at construction — not deep in a request path. - Leaking the selection back into the caller. If client code does
if mode=="air" { svc.setStrategy(new ExpressAir()) }everywhere, you've just moved theif-chain, not removed it. Put selection in one factory/registry (Map<String, ShippingStrategy>) so the branch exists exactly once. - Fat strategy interface. If different strategies need wildly different inputs, you end up passing a giant parameter bag or many
nulls (here,declaredValueis ignored by road/air). Keep the interface to the genuinely common contract; pass a small context object if signatures start diverging. - Strategy explosion for trivial variation. Three one-line formulas as three full classes can be heavier than a lambda/function-value (the Go form). Reserve classes for strategies with real state or dependencies.
- Stateful strategies shared across threads. A strategy object that caches per-call data is a data race if one instance serves concurrent orders. Keep strategies stateless (pure functions of their args) so a single instance is safe to share.
When to use it — and when NOT to
Reach for Strategy when you have a family of interchangeable algorithms for one job (pricing, compression, routing, retry policy), the choice is made at runtime or per-request, and you expect the set to grow. The signal is a growing switch/if on a "kind" that selects behaviour.
vs. if/else (or switch)
The branch chain is fewer lines and zero indirection — perfect when there are 2–3 options that will never grow and never need isolated testing. Cost of moving to Strategy: N extra types/files plus a registry, and one pointer-chase of indirection per call. Choose Strategy when the set is open-ended or each rule deserves its own tests; prefer if/else when the cases are few, stable, and trivial.
vs. Template Method
Template Method also varies behaviour via subclasses, but it fixes the overall skeleton in a base class and lets subclasses fill in steps (compile-time, inheritance). Strategy varies the whole algorithm via composition and can be swapped at runtime. Choose Strategy when you want to change the algorithm on a live object or inject it; prefer Template Method when the high-level flow is invariant and only a few hook steps differ — no runtime swap needed.
vs. State
Structurally identical (an object delegates to a swappable interface). The difference is intent: State objects decide the next state themselves and the client is unaware of transitions; Strategy objects are independent and the client picks one. Choose Strategy when the alternatives don't know about each other; prefer State when behaviour transitions are driven by the object's own lifecycle.
Concrete decision for the shipping case: three pricing rules today, more carriers coming, each rule has its own surcharge/floor logic worth testing alone, and the mode is chosen per order at runtime → Strategy (function-values in Go, classes in Java) wins over the if-chain.
Takeaways
- Strategy = behaviour behind an interface, held as a field, swapped at runtime — the owner delegates and stays unchanged when you add an algorithm.
- Push rule-specific logic (the $25 floor, the 8% surcharge) into the strategy; keep the context dumb.
- Centralise the "which strategy?" decision in one factory/registry, or you've just relocated the if-chain.
- In languages with first-class functions, a function value is a strategy — don't manufacture classes for one-line rules.
Sources: Gamma et al., Design Patterns: Elements of Reusable Object-Oriented Software (the Strategy chapter); Refactoring.Guru, “Strategy”; Freeman & Robson, Head First Design Patterns (2nd ed.), Ch. 1. Re-authored and deepened for this guide — replaced the mismatched travel-planning walkthrough with the logistics shipping-cost example the intro actually poses, added a numeric trace, a naive-version contrast, and Strategy-vs-(if/else, Template Method, State) selection guidance; fixed the mangled “#1## Implementation” heading.
Interview drills & follow-ups
- "Walk the ExpressAir number." subtotal = 15 + 1.20×20 = 39; ×1.08 fuel surcharge = 42.12. The service never sees this formula — it only calls
strategy.cost(...), so the samequote()line yields $30, $42.12, or $25 purely by which object the field points at. - "Strategy vs State — who swaps the pointer?" In Strategy the client picks and assigns the strategy; in State the objects transition themselves and the client is unaware. Same structure, opposite ownership of the swap.
- "When can you share one strategy instance across threads?" Only when it is stateless — a pure function of its arguments, as all three here are. A strategy that caches per-call data becomes a data race under concurrency; give each request its own instance or keep it stateless.
- Null Object as a strategy. Instead of null-checking the field, assign a no-op/default strategy (e.g. a
FreeShippingthat returns 0). The field is then never null, soquote()can never NPE and the "did someone set a strategy?" branch disappears entirely.
🤖 Don't fully get this? Learn it with Claude
Stuck on Strategy Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Strategy Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Strategy Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Strategy Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Strategy Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Strategy Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.