Skip to content

Strategy Pattern

The problem

A class needs to perform some operation, but the exact algorithm depends on context: a payment system needs to support credit cards, PayPal, and crypto; a sorter might need to switch between merge sort and radix sort based on input size; a compressor might switch between gzip and brotli based on content type. The straightforward solution is a long if/else or switch inside the class. That conditional grows with every new algorithm, every branch gets harder to test in isolation, and the class violates the open/closed principle every time you add a variant.

Strategy moves each algorithm into its own class behind a shared interface. The context class holds a reference to that interface, not a concrete implementation. Swapping the algorithm at runtime is a single assignment. Adding a new algorithm requires writing a new class, not modifying the context. Each algorithm is independently testable.

Structure

classDiagram
class PaymentStrategy {
<<interface>>
+pay(amount)
}
class CreditCard {
-cardNumber: string
+pay(amount)
}
class PayPal {
-email: string
+pay(amount)
}
class Crypto {
-address: string
+pay(amount)
}
class Checkout {
-strategy: PaymentStrategy
+setStrategy(strategy)
+processPayment(amount)
}
PaymentStrategy <|-- CreditCard
PaymentStrategy <|-- PayPal
PaymentStrategy <|-- Crypto
Checkout --> PaymentStrategy

When to use

  • A class needs to switch between multiple variants of an algorithm at runtime.
  • You want to isolate algorithm implementations so each can be tested without the context.
  • A family of similar classes differs only in behavior. Strategy replaces subclassing with composition.
  • You are accumulating if/else branches that select different behaviors based on type or configuration.

Implementation

All three show the same payment example: a Checkout context that holds a PaymentStrategy reference and delegates to whichever concrete strategy is active. The Checkout class takes a strategy in its constructor and exposes setStrategy for runtime swaps; none of the concrete strategy classes know about each other or about Checkout. Python uses typing.Protocol rather than an abstract base class, so CreditCard and PayPal satisfy the interface without inheriting from anything. Go interfaces are satisfied implicitly, which means CreditCard, PayPal, and Crypto implement PaymentStrategy without any explicit declaration. Note that a zero-value Checkout with a nil strategy field will panic on the first call in Go: always initialize with a concrete strategy.

idle
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).

Tradeoffs

ProCon
Eliminates conditionals in the context classOne extra class per algorithm
Open/closed: add strategies without changing CheckoutClients must know which strategies exist to pick one
Strategies are independently testableOverkill if there are only two algorithms that rarely change
Works naturally with dependency injectionStateful strategies need care around shared data

Gotchas

  • Strategy and Factory often appear together: a factory creates the strategy, the context uses it. Keep them separate in code; they solve different problems.
  • In Python, a plain function can serve as a strategy when the interface has only one method. Passing credit_card.pay directly is valid duck typing and avoids a wrapper class.
  • Strategies that carry mutable state can cause subtle bugs when the same instance is shared across contexts. Prefer stateless strategies or create fresh instances per use.
  • In Go, a zero-value Checkout with a nil strategy field panics on the first call. Initialize with a sensible default or check for nil in ProcessPayment.
  • Avoid putting infrastructure concerns (logging, metrics) inside strategy implementations. Those cross-cutting concerns belong at the context level or in middleware.

References