bigonotes
CS ConceptsDesign Patterns

Strategy Pattern

Replace if/else chains of algorithms with interchangeable strategy objects that a context can swap at runtime.

6 min readLast updated Design Patterns

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")
  1. Violates Open/Closed: adding BICYCLE means editing Navigator.
  2. God class: real routing algorithms are hundreds of lines each; this file becomes enormous.
  3. Merge conflicts: three developers working on three modes all edit the same file.
  4. Hard to test: you can't test walking logic in isolation.

Structure#

The context holds one strategy at a time and delegates to it.

Strategy: a Navigator context holds one RouteStrategy reference that can point at Driving, Walking or Transit
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 to sorted(), min() and max(): the comparison rule is the strategy.
  • json.dumps(obj, default=...): you supply the algorithm for encoding unknown types.
  • logging.Formatter subclasses: 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/elif or match over 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 if is 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 PriceCalculator does) 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.

Found a mistake? Tell me on LinkedIn ↗

Further reading#