A navigation app can route you by car, on foot or by transit. A checkout can charge a card, PayPal or crypto. When one task has several interchangeable algorithms, putting them all in one if/else makes a giant class that changes every time a new option appears.
Strategy moves each algorithm into its own class behind a shared interface, and lets the main object swap them at runtime. It's the most common pattern in LLD interviews.
Intent#
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. (GoF)
Analogy: route options in a maps app: fastest, shortest, avoid tolls. The app is the same; you pick how it computes the route.
The problem: the if/elif monster#
The naive version puts every algorithm in one method:
class Navigator:
def calculate_route(self, start: str, end: str, mode: str) -> str:
if mode == "DRIVING":
# ... 50 lines of traffic calculations ...
return f"Driving route from {start} to {end} via Highway 1."
elif mode == "WALKING":
# ... 50 lines of sidewalk and trail logic ...
return f"Walking route from {start} to {end} via Park Trail."
elif mode == "TRANSIT":
# ... 50 lines of bus schedule lookups ...
return f"Transit route from {start} to {end} via Bus 42."
else:
raise ValueError("Unsupported routing mode")
- Violates Open/Closed: adding
BICYCLEmeans editingNavigator. - God class: real routing algorithms are hundreds of lines each; this file becomes enormous.
- Merge conflicts: three developers working on three modes all edit the same file.
- Hard to test: you can't test walking logic in isolation.
Structure#
The context holds one strategy at a time and delegates to it.
| Participant | Role |
|---|---|
| Strategy | The interface every algorithm implements (RouteStrategy) |
| Concrete strategies | One class per algorithm (DrivingStrategy, WalkingStrategy) |
| Context | Holds a strategy reference, delegates the work to it, and allows swapping (Navigator) |
Example 1: navigation#
Each routing mode becomes its own class:
from abc import ABC, abstractmethod
class RouteStrategy(ABC):
@abstractmethod
def build_route(self, start: str, end: str) -> str: ...
class DrivingStrategy(RouteStrategy):
def build_route(self, start: str, end: str) -> str:
return f"Driving route from {start} to {end} via Highway 1."
class WalkingStrategy(RouteStrategy):
def build_route(self, start: str, end: str) -> str:
return f"Walking route from {start} to {end} via Park Trail."
class TransitStrategy(RouteStrategy):
def build_route(self, start: str, end: str) -> str:
return f"Transit route from {start} to {end} via Bus 42."
class Navigator:
def __init__(self, strategy: RouteStrategy): # inject a default
self._strategy = strategy
# Swap at runtime
def set_strategy(self, strategy: RouteStrategy) -> None:
self._strategy = strategy
def calculate_route(self, start: str, end: str) -> str:
return self._strategy.build_route(start, end) # delegate blindly
navigator = Navigator(DrivingStrategy())
print(navigator.calculate_route("Home", "Office")) # Driving route ...
# Traffic is bad; the user taps "Walk"
navigator.set_strategy(WalkingStrategy())
print(navigator.calculate_route("Home", "Office")) # Walking route ...
Adding BicycleStrategy is a new class; Navigator doesn't change. Each strategy has one job and can be unit-tested alone.
Example 2: checkout payments#
The shopping cart is the context; each payment method is a strategy.
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def validate(self) -> bool: ...
@abstractmethod
def pay(self, amount: float) -> None: ...
@property
@abstractmethod
def payment_type(self) -> str: ...
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, cvv: str, expiry: str):
self._card_number = card_number
self._cvv = cvv
self._expiry = expiry
def validate(self) -> bool:
return len(self._card_number) == 16 and len(self._cvv) == 3
def pay(self, amount: float) -> None:
if not self.validate():
print("Credit card payment failed")
return
print(f"Charged ${amount:.2f} to card ending "
f"{self._card_number[-4:]}")
@property
def payment_type(self) -> str:
return "Credit Card"
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str, password: str):
self._email = email
self._password = password
def validate(self) -> bool:
return len(self._password) >= 8 # stand-in for a real login
def pay(self, amount: float) -> None:
if self.validate():
print(f"Transferred ${amount:.2f} from PayPal {self._email}")
@property
def payment_type(self) -> str:
return "PayPal"
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self._wallet_address = wallet_address
def validate(self) -> bool:
return len(self._wallet_address) >= 26
def pay(self, amount: float) -> None:
if self.validate():
print(f"Sent ${amount:.2f} in crypto "
f"(network fee ${amount * 0.01:.2f})")
@property
def payment_type(self) -> str:
return "Crypto"
# Context
class ShoppingCart:
def __init__(self):
self._prices: list[float] = []
self._payment_strategy: PaymentStrategy | None = None
def add_item(self, price: float) -> None:
self._prices.append(price)
def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
self._payment_strategy = strategy
def checkout(self) -> None:
if self._payment_strategy is None:
raise RuntimeError("Select a payment method first")
# The cart doesn't know HOW payment works
self._payment_strategy.pay(sum(self._prices))
cart = ShoppingCart()
cart.add_item(999.99)
cart.add_item(29.99)
cart.set_payment_strategy(
CreditCardPayment("1234567812345678", "123", "12/27"))
cart.checkout()
# Change of mind
cart.set_payment_strategy(
PayPalPayment("user@email.com", "securepassword"))
cart.checkout()
The same structure fits any "pick one behavior" problem. In a game, a MapStrategy (desert, ice, jungle) could supply terrain rendering, physics rules and enemy spawning, and a power-up could swap the map mid-game with game.switch_map(IceMapStrategy()).
Example 3: functions as strategies#
If the strategy has one method, you don't need a class per algorithm. Any callable (a lambda or a plain function) is a strategy:
from typing import Callable
Discount = Callable[[float], float]
class PriceCalculator:
def __init__(self):
self._discount: Discount = lambda p: p # default: no discount
def set_discount(self, discount: Discount) -> None:
self._discount = discount
def calculate(self, price: float) -> float:
return self._discount(price)
def tiered(p: float) -> float:
if p > 100:
return p * 0.7
if p > 50:
return p * 0.9
return p
calc = PriceCalculator()
calc.set_discount(lambda p: p * 0.9) # regular customer: 10% off
calc.set_discount(lambda p: p * 0.8) # premium: 20% off
calc.set_discount(tiered) # a named function works too
print(calc.calculate(200)) # 140.0
Template#
The bare skeleton, ready to adapt:
from abc import ABC, abstractmethod
class Strategy(ABC):
@abstractmethod
def execute(self) -> None: ...
class ConcreteStrategyA(Strategy):
def execute(self) -> None:
print("A")
class ConcreteStrategyB(Strategy):
def execute(self) -> None:
print("B")
class Context:
def __init__(self, strategy: Strategy):
self._strategy = strategy
def set_strategy(self, strategy: Strategy) -> None:
self._strategy = strategy
def perform_operation(self) -> None:
self._strategy.execute()
ctx = Context(ConcreteStrategyA())
ctx.perform_operation()
ctx.set_strategy(ConcreteStrategyB())
ctx.perform_operation()
Real-world examples#
You've likely used Strategy already:
- The
key=function passed tosorted(),min()andmax(): the comparison rule is the strategy. json.dumps(obj, default=...): you supply the algorithm for encoding unknown types.logging.Formattersubclasses: the same handler, different formatting algorithms.- Django's pluggable authentication backends.
- Business: pricing and discount rules, game AI (aggressive, defensive), export formats (PDF, Excel, CSV), compression algorithms, validation rules.
When to use it#
Strategy fits when:
- There are several ways to do the same task, and you pick one at runtime (often the user picks).
- A class has a growing
if/eliformatchover algorithm types. - You want to isolate algorithm details and test each one alone.
- Algorithms should vary independently from the code that uses them.
When not to use it#
Skip it when:
- There are only two simple, stable branches. An
ifis clearer. - The algorithm never changes.
- The algorithms need very different inputs, which forces a bloated shared interface.
Pitfalls#
Watch for these trade-offs:
- Clients must know the strategies to choose one. Pair Strategy with a factory when the choice comes from config or user input.
- Class count grows with the number of algorithms (plain functions help).
- Data passing. Context and strategy must agree on what data flows between them; passing the whole context couples them.
- Null strategy. Give the context a sensible default (as
PriceCalculatordoes) or fail loudly.
Strategy vs similar patterns#
Several patterns look like Strategy at first glance:
| Pattern | Difference |
|---|---|
| State | Same structure, different intent: the client picks a strategy; a state object changes itself as internal state changes |
| Template Method | Replaces some steps via inheritance; Strategy replaces the whole algorithm via composition |
| Command | Wraps a request (what to do, possibly undoable); Strategy is how to do one task |
| Bridge | Two hierarchies vary; in Strategy only the algorithm does |
Strategy is "composition over inheritance": the object is composed with a behavior rather than inheriting it.
Key takeaways#
- Strategy = one interface, many interchangeable algorithm classes, and a context that delegates to one of them.
- It replaces conditionals with polymorphism and satisfies Open/Closed.
- Strategies can be swapped at runtime via a setter or chosen via constructor injection.
- With single-method interfaces, lambdas or plain functions are strategies.
- Strategy and State look alike; the difference is who chooses the behavior.