Strategy
Defines a family of algorithms and makes them interchangeable.
Strategy Pattern: A Complete Guide with Examples in 11 Programming Languages
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable. Instead of hardcoding a specific algorithm into a class — or worse, piling up if/else branches to pick between implementations — the context delegates the work to a strategy object that can be swapped freely at runtime. The context doesn't know or care which algorithm it's using. It just calls the strategy. The caller decides which strategy fits.
In this comprehensive guide, we'll explore the Strategy Pattern with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.
Table of Contents
- What is the Strategy Pattern?
- Why Use the Strategy Pattern?
- Strategy Pattern Comparison
- Strategy Pattern Explained
- Class Diagram
- Beginner-Friendly Example
- Production-Ready Example
- Real-World Use Cases
- Language-Specific Mistakes and Anti-Patterns
- Frequently Asked Questions
- Key Takeaways
What is the Strategy Pattern?
The Strategy Pattern solves a deceptively common problem: you have a class that needs to perform a task, but the way it performs that task should vary depending on circumstances — and those variations are real algorithms with their own logic, not just simple configuration tweaks.
The naive approach is to add flags and conditionals: if (sortMethod === "quick") { ... } else if (sortMethod === "merge") { ... }. This works for one algorithm, but it scales terribly. Every new variant requires editing the class. Every variant's code is tangled together. The class becomes responsible for every algorithm it might ever use — a violation of Single Responsibility. Testing one algorithm forces you to navigate the others.
The Strategy Pattern resolves this by extracting each algorithm into its own class behind a shared interface. The context holds a reference to the interface — not any specific implementation. At runtime, the caller injects the desired strategy. The context calls the interface and the right algorithm runs. Adding a new algorithm is adding a new class — not editing existing ones.
Think of it like a GPS navigation app. The same route can be calculated as the fastest path, the shortest path, the most scenic route, or the one that avoids highways. The map (context) doesn't contain all four algorithms. It delegates to a routing strategy (interface), and the user picks which one to inject. The map never changes; the strategies do.
Why Use the Strategy Pattern?
The Strategy Pattern offers several compelling benefits:
- Open/Closed Principle: Add new algorithms by adding new strategy classes — zero modifications to the context or existing strategies
- Single Responsibility: Each strategy class owns exactly one algorithm — its implementation, edge cases, and optimizations stay contained
- Runtime Interchangeability: Swap algorithms at runtime based on user preferences, configuration, environment, or data characteristics
- Elimination of Conditionals: Replace if/else chains that pick between algorithms with clean delegation — the context is free of algorithmic logic entirely
- Isolated Testability: Each strategy can be unit-tested independently — no need to construct a complex context to reach a specific algorithm branch
- Reusability: Strategies are independent objects that can be reused across different contexts — a
QuickSortStrategyisn't tied to any specific list class
Strategy Pattern Comparison
Let's compare the Strategy Pattern with related patterns:
| Pattern | Purpose | Who Assigns Behavior | Self-Transitions | Use Case |
|---|---|---|---|---|
| Strategy | Interchangeable algorithms | Client assigns externally | Never | Sorting, routing, payment, compression |
| State | Behavior based on lifecycle phase | States self-transition | Yes | FSMs, orders, traffic lights, players |
| Template Method | Algorithm skeleton with variable steps | Subclass overrides steps | N/A (inheritance) | Report generation, data parsing pipelines |
| Command | Encapsulate a request as an object | Invoker decides execution | N/A | Undo/redo, queues, transactions |
| Decorator | Add behavior by wrapping | Client composes wrappers | N/A | Logging, caching, formatting layers |
Key Distinctions:
- Strategy vs. State: This is the most commonly confused pair. They are structurally identical — both delegate to a swappable object. The difference is who changes the behavior and why. A Strategy is selected by the client and assigned to the context; it doesn't change itself. A State changes autonomously based on events the object controls — states self-transition. Use Strategy when the algorithm is a choice; use State when the behavior is a phase.
- Strategy vs. Template Method: Template Method uses inheritance to vary algorithm steps — the base class defines the skeleton and subclasses fill in the blanks. Strategy uses composition — the strategy object is the variable part. Strategy is more flexible (swap at runtime, no subclassing required) but requires an extra object. Template Method is simpler when the algorithm skeleton is fixed and the variants are always known at compile time.
- Strategy vs. Decorator: Decorator adds behavior on top of an existing operation (logging, caching, validation around a call). Strategy replaces the core algorithm entirely. Use Decorator when you want to augment; use Strategy when you want to substitute.
Strategy Pattern Explained
The Strategy Pattern involves three participants:
Core Components:
-
Strategy Interface: Declares the method (or methods) that all concrete strategies implement. The context only knows this interface — never any concrete type. The interface's method signature defines the algorithm's contract: what goes in (parameters) and what comes out (return type).
-
Concrete Strategies: Each concrete strategy class implements the Strategy interface with one specific algorithm. The class is self-contained — its logic, helper methods, and any algorithm-specific configuration live entirely within it. Concrete strategies can have their own constructors, injecting algorithm-specific parameters (e.g., a
ThresholdCompressionStrategythat takes a compression level). -
Context: The object that uses a strategy. It holds a reference to the Strategy interface. All algorithm-related work is delegated to this reference. The context may provide data to the strategy via method arguments or expose its own state for the strategy to read. It does not know which concrete strategy is in use — it calls the interface and trusts the runtime binding.
Strategy Assignment Strategies (no pun intended):
- Constructor injection: The strategy is passed in when the context is created. Good for contexts where the algorithm is fixed for the object's lifetime.
- Setter injection: The context exposes a
setStrategy()method. The strategy can be swapped after construction. Good for contexts where the algorithm should change at runtime based on user choice or data characteristics. - Method-level injection: The strategy is passed as an argument to the specific method that uses it. The context holds no long-term reference. Good when different calls to the same method might use different strategies.
Strategy Pattern Variants:
- Stateless strategies: The strategy method is pure — it takes inputs and returns outputs with no side effects or instance state. Can be shared as singletons or even represented as functions/lambdas.
- Stateful strategies: The strategy holds configuration or accumulated state (e.g., a
MachineLearningRouteStrategythat caches learned route data). Each context may need its own strategy instance. - Composable strategies: Strategies that wrap or chain other strategies. A
CachedSortStrategywraps aQuickSortStrategy— sort the first time, return the cached result thereafter. - Function-as-Strategy: In languages with first-class functions, a strategy can be a plain function or lambda rather than a class. This is idiomatic in Python, JavaScript, Go, Kotlin, Swift, and Rust.
Class Diagram
Here's the UML class diagram showing the Strategy Pattern structure:
Beginner-Friendly Example
Let's start with the most intuitive Strategy example: a sorter that can swap its sorting algorithm at runtime. The Sorter context holds a SortStrategy. The caller provides data and a strategy. The sorter delegates — it never contains any sorting logic itself.
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
import random
# ── Strategy Interface ─────────────────────────────────────────────
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: List[int]) -> List[int]:
"""Sort a list of integers and return the sorted list."""
# ── Concrete Strategies ────────────────────────────────────────────
class BubbleSortStrategy(SortStrategy):
"""O(n²) — simple but slow; good for nearly-sorted data."""
def sort(self, data: List[int]) -> List[int]:
result = data.copy()
n = len(result)
for i in range(n):
for j in range(0, n - i - 1):
if result[j] > result[j + 1]:
result[j], result[j + 1] = result[j + 1], result[j]
print(f" BubbleSort: sorted {len(result)} elements")
return result
class QuickSortStrategy(SortStrategy):
"""O(n log n) average — fast general-purpose sort."""
def sort(self, data: List[int]) -> List[int]:
result = data.copy()
self._quicksort(result, 0, len(result) - 1)
print(f" QuickSort: sorted {len(result)} elements")
return result
def _quicksort(self, arr: List[int], low: int, high: int) -> None:
if low < high:
pivot_idx = self._partition(arr, low, high)
self._quicksort(arr, low, pivot_idx - 1)
self._quicksort(arr, pivot_idx + 1, high)
def _partition(self, arr: List[int], low: int, high: int) -> int:
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
class MergeSortStrategy(SortStrategy):
"""O(n log n) guaranteed — stable sort, good for linked data."""
def sort(self, data: List[int]) -> List[int]:
result = self._mergesort(data.copy())
print(f" MergeSort: sorted {len(result)} elements")
return result
def _mergesort(self, arr: List[int]) -> List[int]:
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = self._mergesort(arr[:mid])
right = self._mergesort(arr[mid:])
return self._merge(left, right)
def _merge(self, left: List[int], right: List[int]) -> List[int]:
result, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i]); i += 1
else:
result.append(right[j]); j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# ── Context ─────────────────────────────────────────────────────────
class Sorter:
"""Context — delegates all sorting to the current strategy."""
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def sort(self, data: List[int]) -> List[int]:
return self._strategy.sort(data)
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
data = random.sample(range(1, 101), 10)
print(f"Original: {data}\n")
sorter = Sorter(BubbleSortStrategy())
print(f"Result: {sorter.sort(data)}\n")
sorter.set_strategy(QuickSortStrategy())
print(f"Result: {sorter.sort(data)}\n")
sorter.set_strategy(MergeSortStrategy())
print(f"Result: {sorter.sort(data)}")
Production-Ready Example
Now let's build a payment processing system — a richer, more realistic application of the Strategy Pattern. An e-commerce checkout can accept multiple payment methods: credit card, PayPal, cryptocurrency, and bank transfer. Each method has its own validation, fee calculation, and processing logic. The PaymentProcessor context delegates all payment work to the current PaymentStrategy. The checkout never knows which payment method is in use.
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
from decimal import Decimal, ROUND_HALF_UP
import re
# ── Value Objects ─────────────────────────────────────────────────
@dataclass(frozen=True)
class PaymentResult:
success: bool
transaction_id: Optional[str]
amount_charged: Decimal
fee: Decimal
message: str
def display(self) -> None:
status = "✅ SUCCESS" if self.success else "❌ FAILED"
print(f" {status}: {self.message}")
if self.success:
print(f" Transaction ID : {self.transaction_id}")
print(f" Amount charged : ${self.amount_charged:.2f}")
print(f" Processing fee : ${self.fee:.2f}")
@dataclass
class Order:
order_id: str
amount: Decimal
description: str
# ── Strategy Interface ────────────────────────────────────────────
class PaymentStrategy(ABC):
@abstractmethod
def validate(self) -> tuple[bool, str]:
"""Validate payment details. Returns (is_valid, error_message)."""
@abstractmethod
def calculate_fee(self, amount: Decimal) -> Decimal:
"""Calculate processing fee for the given amount."""
@abstractmethod
def process(self, order: Order) -> PaymentResult:
"""Process the payment and return a result."""
@property
@abstractmethod
def method_name(self) -> str:
"""Human-readable name of this payment method."""
# ── Concrete Strategies ────────────────────────────────────────────
class CreditCardStrategy(PaymentStrategy):
FEE_RATE = Decimal("0.029") # 2.9%
FLAT_FEE = Decimal("0.30") # $0.30 flat
def __init__(
self,
card_number: str,
expiry: str, # MM/YY
cvv: str,
cardholder: str,
) -> None:
self._card_number = card_number.replace(" ", "")
self._expiry = expiry
self._cvv = cvv
self._cardholder = cardholder
@property
def method_name(self) -> str:
return f"Credit Card (****{self._card_number[-4:]})"
def validate(self) -> tuple[bool, str]:
if not re.fullmatch(r"\d{13,19}", self._card_number):
return False, "Invalid card number format"
if not re.fullmatch(r"(0[1-9]|1[0-2])\/\d{2}", self._expiry):
return False, "Invalid expiry format (expected MM/YY)"
if not re.fullmatch(r"\d{3,4}", self._cvv):
return False, "Invalid CVV"
if not self._cardholder.strip():
return False, "Cardholder name is required"
return True, ""
def calculate_fee(self, amount: Decimal) -> Decimal:
fee = (amount * self.FEE_RATE + self.FLAT_FEE)
return fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def process(self, order: Order) -> PaymentResult:
valid, error = self.validate()
if not valid:
return PaymentResult(False, None, Decimal("0"), Decimal("0"), f"Validation failed: {error}")
fee = self.calculate_fee(order.amount)
total = order.amount + fee
# Simulate gateway call
txn_id = f"CC-{order.order_id}-{self._card_number[-4:]}"
return PaymentResult(
success=True, transaction_id=txn_id,
amount_charged=total, fee=fee,
message=f"Card payment authorized via {self.method_name}"
)
class PayPalStrategy(PaymentStrategy):
FEE_RATE = Decimal("0.034") # 3.4%
FLAT_FEE = Decimal("0.30")
def __init__(self, email: str, password: str) -> None:
self._email = email
self._password = password # Would be a token in production
@property
def method_name(self) -> str:
return f"PayPal ({self._email})"
def validate(self) -> tuple[bool, str]:
if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", self._email):
return False, "Invalid PayPal email address"
if len(self._password) < 8:
return False, "PayPal token is invalid"
return True, ""
def calculate_fee(self, amount: Decimal) -> Decimal:
fee = (amount * self.FEE_RATE + self.FLAT_FEE)
return fee.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def process(self, order: Order) -> PaymentResult:
valid, error = self.validate()
if not valid:
return PaymentResult(False, None, Decimal("0"), Decimal("0"), f"Validation failed: {error}")
fee = self.calculate_fee(order.amount)
total = order.amount + fee
txn_id = f"PP-{order.order_id}-{hash(self._email) % 100000:05d}"
return PaymentResult(
success=True, transaction_id=txn_id,
amount_charged=total, fee=fee,
message=f"PayPal payment completed for {self._email}"
)
class CryptoStrategy(PaymentStrategy):
NETWORK_FEE = Decimal("0.0005") # Fixed crypto network fee in BTC equivalent
def __init__(self, wallet_address: str, currency: str = "BTC") -> None:
self._wallet = wallet_address
self._currency = currency.upper()
@property
def method_name(self) -> str:
return f"{self._currency} ({self._wallet[:8]}…{self._wallet[-4:]})"
def validate(self) -> tuple[bool, str]:
if len(self._wallet) < 26:
return False, "Wallet address too short"
if self._currency not in ("BTC", "ETH", "USDC", "USDT"):
return False, f"Unsupported currency: {self._currency}"
return True, ""
def calculate_fee(self, amount: Decimal) -> Decimal:
# Flat network fee regardless of amount
return self.NETWORK_FEE.quantize(Decimal("0.000001"))
def process(self, order: Order) -> PaymentResult:
valid, error = self.validate()
if not valid:
return PaymentResult(False, None, Decimal("0"), Decimal("0"), f"Validation failed: {error}")
fee = self.calculate_fee(order.amount)
total = order.amount + fee
txn_id = f"CRYPTO-{order.order_id}-{self._currency}-{self._wallet[-6:]}"
return PaymentResult(
success=True, transaction_id=txn_id,
amount_charged=total, fee=fee,
message=f"{self._currency} payment to {self._wallet[:8]}… confirmed"
)
class BankTransferStrategy(PaymentStrategy):
FEE_FLAT = Decimal("1.50") # Fixed flat fee for bank transfers
def __init__(self, account_number: str, routing_number: str, account_name: str) -> None:
self._account_number = account_number
self._routing_number = routing_number
self._account_name = account_name
@property
def method_name(self) -> str:
return f"Bank Transfer (****{self._account_number[-4:]})"
def validate(self) -> tuple[bool, str]:
if not re.fullmatch(r"\d{4,17}", self._account_number):
return False, "Invalid account number"
if not re.fullmatch(r"\d{9}", self._routing_number):
return False, "Routing number must be 9 digits"
if not self._account_name.strip():
return False, "Account name is required"
return True, ""
def calculate_fee(self, amount: Decimal) -> Decimal:
return self.FEE_FLAT
def process(self, order: Order) -> PaymentResult:
valid, error = self.validate()
if not valid:
return PaymentResult(False, None, Decimal("0"), Decimal("0"), f"Validation failed: {error}")
fee = self.calculate_fee(order.amount)
total = order.amount + fee
txn_id = f"ACH-{order.order_id}-{self._account_number[-4:]}"
return PaymentResult(
success=True, transaction_id=txn_id,
amount_charged=total, fee=fee,
message=f"ACH bank transfer initiated to {self._account_name}"
)
# ── Context ─────────────────────────────────────────────────────────
class PaymentProcessor:
"""Context — processes orders using the injected payment strategy."""
def __init__(self, strategy: PaymentStrategy) -> None:
self._strategy = strategy
def set_strategy(self, strategy: PaymentStrategy) -> None:
self._strategy = strategy
print(f" [Payment method changed to: {strategy.method_name}]")
def process_order(self, order: Order) -> PaymentResult:
print(f"\n Processing order {order.order_id}: {order.description}")
print(f" Amount: ${order.amount:.2f}")
print(f" Payment method: {self._strategy.method_name}")
print(f" Expected fee: ${self._strategy.calculate_fee(order.amount):.4f}")
result = self._strategy.process(order)
result.display()
return result
# ── Client ─────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== Payment Processing System ===")
order = Order("ORD-001", Decimal("99.99"), "Premium subscription × 1 year")
# Credit card payment
cc = CreditCardStrategy("4111111111111111", "12/26", "123", "Jane Doe")
processor = PaymentProcessor(cc)
processor.process_order(order)
# Switch to PayPal
print()
paypal = PayPalStrategy("jane@example.com", "securetoken")
processor.set_strategy(paypal)
processor.process_order(order)
# Switch to crypto
print()
crypto = CryptoStrategy("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "BTC")
processor.set_strategy(crypto)
processor.process_order(order)
# Invalid card — validation failure
print()
bad_card = CreditCardStrategy("1234", "99/99", "AB", "")
processor.set_strategy(bad_card)
processor.process_order(order)
Real-World Use Cases
1. Payment Gateways in E-Commerce
Every serious checkout page supports multiple payment methods: credit card, PayPal, Apple Pay, Google Pay, bank transfer, buy-now-pay-later. Each method has its own API, fee structure, validation rules, and error handling. The Strategy Pattern encapsulates each payment method as an independent class. Adding a new payment provider — say, Stripe or Klarna — means adding one new strategy class, not modifying the checkout logic.
2. Sorting and Data Processing Pipelines
Standard libraries across all languages implement their sort functions with interchangeable algorithms under the hood — introsort for general cases, timsort for partially sorted data, counting sort for integer ranges. Application-level data pipelines use the Strategy Pattern to select the right algorithm: sort small arrays with insertion sort, large arrays with merge sort, nearly-sorted arrays with timsort. The pipeline never changes; the strategy adapts to the data.
3. File Compression and Encoding
Archiving tools and media processors support multiple compression algorithms: ZIP, GZIP, LZ4, Brotli, Zstandard. Each algorithm has different trade-offs in speed, compression ratio, and CPU usage. The Compressor context accepts a CompressionStrategy. Production systems often select the strategy based on file type, target network speed, or CPU availability — a LZ4Strategy for real-time streaming, a ZstandardStrategy for cold storage.
4. Route Calculation in Navigation
Navigation apps like Google Maps and Waze offer multiple routing strategies: fastest route, shortest distance, avoiding highways, avoiding tolls, bicycle-friendly, and walking directions. The map engine is the context; the routing algorithm is the strategy. The user picks the strategy (or the app picks based on transport mode and conditions). The map renders whatever the strategy returns — it doesn't contain any routing logic.
5. Discount and Pricing Engines
E-commerce platforms support complex pricing rules: percentage discounts, flat deductions, buy-one-get-one, tiered pricing, loyalty points redemption, coupon codes. Each pricing rule is a DiscountStrategy. The cart total is computed by applying the active strategies in sequence. Adding a seasonal sale or a new loyalty tier is adding a new strategy class — no touching the cart calculation engine.
6. Authentication and Authorization
Systems supporting multiple login methods — username/password, OAuth2, SAML, LDAP, biometric — use the Strategy Pattern. The Authenticator context accepts an AuthStrategy. OAuth2Strategy handles token exchange; LDAPStrategy queries the corporate directory; BiometricStrategy validates a fingerprint signature. The session management layer never knows which authentication method is in use.
7. Logging and Observability
Applications that log to multiple backends — file, console, database, cloud service (Datadog, Splunk, CloudWatch) — use the Strategy Pattern. The logger context delegates write calls to a LogStrategy. A development environment uses ConsoleLogStrategy; production uses CloudWatchStrategy. The application code calls logger.log(level, message) — the destination is a pluggable strategy, configurable per environment.
Language-Specific Mistakes and Anti-Patterns
❌ Mistake #1: Using a String Parameter to Select the Algorithm
# BAD: A string flag selects the algorithm inside the sorter — Strategy Pattern reversed
class Sorter:
def sort(self, data: list, method: str = "quicksort") -> list:
if method == "quicksort":
return self._quicksort(data)
elif method == "mergesort":
return self._mergesort(data)
elif method == "bubblesort":
return self._bubblesort(data)
else:
raise ValueError(f"Unknown sort method: {method}")
# Adding a new method means editing Sorter — Open/Closed violated
# GOOD: The algorithm is a strategy object, not a string
class Sorter:
def __init__(self, strategy: SortStrategy) -> None:
self._strategy = strategy
def sort(self, data: list) -> list:
return self._strategy.sort(data) # Delegate — Sorter never knows the algorithm
❌ Mistake #2: Not Using Callables as Strategies for Stateless Algorithms
# VERBOSE: A full class for a stateless, single-method strategy in Python
class UpperCaseStrategy:
def format(self, text: str) -> str:
return text.upper()
class LowerCaseStrategy:
def format(self, text: str) -> str:
return text.lower()
formatter = TextFormatter(UpperCaseStrategy())
# BETTER: Use a callable (function/lambda) — Python's first-class functions ARE strategies
from typing import Callable
class TextFormatter:
def __init__(self, strategy: Callable[[str], str]) -> None:
self._strategy = strategy
def format(self, text: str) -> str:
return self._strategy(text)
formatter = TextFormatter(str.upper) # Built-in method as strategy
formatter.set_strategy(lambda s: s.title()) # Lambda as strategy
formatter.set_strategy(str.lower) # Another built-in
❌ Mistake #3: Putting Algorithm Selection Logic Back into the Context
# BAD: Context auto-selects strategy based on data — defeats the purpose of the pattern
class Sorter:
def sort(self, data: list) -> list:
if len(data) < 10:
return BubbleSortStrategy().sort(data) # Context choosing for itself!
elif len(data) < 1000:
return QuickSortStrategy().sort(data)
else:
return MergeSortStrategy().sort(data)
# Context now knows about all strategies — tightly coupled again
# GOOD: A factory or the caller decides the strategy based on data characteristics
def pick_strategy(data: list) -> SortStrategy:
if len(data) < 10: return BubbleSortStrategy()
if len(data) < 1000: return QuickSortStrategy()
return MergeSortStrategy()
sorter = Sorter(pick_strategy(data)) # Factory picks; context stays clean
Frequently Asked Questions
Q1: What is the difference between the Strategy Pattern and simple if/else or switch statements?
Both select between alternatives. The key difference is where the alternatives live and how open the system is to new ones.
With if/else or switch, all alternatives live inside one method in one class. Adding a new algorithm means editing that class — recompiling, re-testing, risking regressions in other algorithms. The class is responsible for every algorithm it might ever use, violating Single Responsibility.
With the Strategy Pattern, each alternative is an independent class. Adding a new algorithm means adding a new class — the context and existing strategies are untouched. The system is open for extension without modification. Each algorithm is testable in isolation.
For two or three stable alternatives that will never change, a simple conditional is genuinely simpler. The Strategy Pattern pays off when alternatives grow, when they need to be independently tested, or when they should be configurable at runtime.
Q2: When should I use a function/lambda instead of a full strategy class?
Use a function or lambda when the strategy is stateless — it takes input, returns output, and holds no configuration or accumulated state between calls. Text formatting, simple transformations, and one-liner predicates are ideal function strategies. In Python, JavaScript, Go, Kotlin, Swift, and Dart, passing a function directly is idiomatic and avoids boilerplate.
Use a full class when:
- The strategy holds configuration (
CompressionStrategy(level=9)) - The strategy accumulates state between calls (
CachingRouteStrategythat learns from history) - The strategy needs multiple methods (
PaymentStrategywithvalidate(),calculateFee(),process()) - The strategy needs to be serializable, injectable via DI framework, or discoverable via reflection
Q3: How is Strategy different from State? They look the same in code.
They ARE structurally identical — both hold a reference to an interface and delegate behavior. The difference is entirely about intent and autonomy:
Strategy is a choice made by the caller. The caller picks the algorithm and injects it. The strategy never changes itself. It has no awareness of other strategies. It doesn't know about the context's lifecycle.
State is a phase that the object controls. States self-transition — they know which state comes next and trigger the switch themselves. States are aware of the context's lifecycle and each other's existence (at least their names). Adding a new state may require updating existing states to account for the new transition.
If you catch yourself writing context.setStrategy(new SomeState()) and the strategy is deciding when to switch, it has become a State.
Q4: Should the context expose setStrategy() or should the strategy be fixed at construction?
It depends on whether the algorithm needs to change after the object is created.
Use constructor injection only when:
- The algorithm is part of the object's identity (a
CreditCardPaymentProcessoris always credit card) - The algorithm must never change after creation (immutability by design)
- You're using a DI container that wires strategies at startup
Use setter injection (setStrategy()) when:
- The algorithm should change based on user choice, configuration, or runtime conditions
- The same context object handles different algorithms over its lifetime (a single
PaymentProcessorthat supports switching payment methods mid-session)
Many real systems use both: a default strategy injected at construction, with setStrategy() available for runtime changes.
Q5: Can strategies call each other or be composed?
Yes — this is the Composable Strategy or Chain of Strategies variant.
class CachedSortStrategy(SortStrategy):
"""Wraps another strategy and caches the result."""
def __init__(self, inner: SortStrategy) -> None:
self._inner = inner
self._cache: dict = {}
def sort(self, data: list) -> list:
key = tuple(data)
if key not in self._cache:
self._cache[key] = self._inner.sort(data)
return self._cache[key]
# Composition: caching layer wraps the real algorithm
strategy = CachedSortStrategy(QuickSortStrategy())
sorter = Sorter(strategy)
This is the Decorator pattern applied to a Strategy. It's powerful for adding logging, caching, retry logic, or metrics around any algorithm without modifying the algorithm itself.
Q6: How do I avoid having the context know about concrete strategy types?
Keep concrete strategy classes non-public. Expose them only through a factory function or a DI container:
// External code never imports BubbleSortStrategy directly
SortStrategy strategy = SortStrategyFactory.forDataSize(data.length);
Sorter sorter = new Sorter(strategy);
In the factory, concrete types live behind the factory's package boundary. Consumers depend only on the interface. This enforces the Dependency Inversion Principle — high-level code depends on the SortStrategy abstraction, not any concrete sort class.
Q7: How do I test code that uses the Strategy Pattern?
The Strategy Pattern dramatically improves testability in two directions:
Testing the context: Mock or stub the strategy interface. The context's behavior (handling results, error propagation, logging) can be tested without a real algorithm.
class MockSortStrategy(SortStrategy):
def __init__(self, return_value: list) -> None:
self.was_called = False
self._return_value = return_value
def sort(self, data: list) -> list:
self.was_called = True
return self._return_value
def test_sorter_delegates_to_strategy():
mock = MockSortStrategy([1, 2, 3])
sorter = Sorter(mock)
result = sorter.sort([3, 1, 2])
assert mock.was_called
assert result == [1, 2, 3]
Testing each strategy independently: Create an instance of each concrete strategy directly. Feed it inputs and assert outputs — no context needed. Each algorithm becomes a pure unit test with no setup overhead.
Q8: When should I NOT use the Strategy Pattern?
Avoid the Strategy Pattern when:
- You have only one algorithm that will never change: Adding a strategy interface with one implementation is pure indirection overhead — no benefit.
- The algorithms are trivially short (one line): Wrapping
data.sort()in a strategy class is overengineered — use a lambda or just call it directly. - The variation is configuration, not algorithm: If the difference between "strategies" is just a parameter value (compression level 1 vs. 9), use a constructor parameter — not a strategy class.
- All variants are always known at compile time and will never change: An exhaustive
enumwith awhen/matchexpression in Kotlin, Rust, or Swift may be simpler and safer (compiler-enforced exhaustiveness) for a small, sealed set of options. - Performance is critical and virtual dispatch overhead matters: In hot paths with millions of calls per second, the indirection of interface dispatch has a cost. Profile first; only eliminate the pattern if the profiler confirms it's the bottleneck.
Q9: How do I handle strategy selection based on data characteristics?
Extract the selection logic into a Strategy Factory or Strategy Selector. This keeps the context clean and puts selection logic in one dedicated place:
class SortStrategySelector:
@staticmethod
def for_data(data: list) -> SortStrategy:
n = len(data)
if n <= 10:
return BubbleSortStrategy() # Simple for tiny lists
if _is_nearly_sorted(data):
return InsertionSortStrategy() # Fast for nearly-sorted
if n > 100_000:
return MergeSortStrategy() # Stable for very large lists
return QuickSortStrategy() # Fast general-purpose default
sorter = Sorter(SortStrategySelector.for_data(my_data))
The selector can even be a strategy itself — injected into the context and called before each operation.
Q10: How do I make strategies configurable from external configuration (config files, environment)?
Map configuration values to strategy instances in a registry or factory:
STRATEGY_REGISTRY: dict[str, type[SortStrategy]] = {
"bubble": BubbleSortStrategy,
"quick": QuickSortStrategy,
"merge": MergeSortStrategy,
}
strategy_name = os.getenv("SORT_STRATEGY", "quick")
strategy_class = STRATEGY_REGISTRY.get(strategy_name)
if strategy_class is None:
raise ValueError(f"Unknown sort strategy: {strategy_name}")
sorter = Sorter(strategy_class())
For strategies that require constructor arguments, use a factory function per entry:
PAYMENT_REGISTRY: dict[str, Callable[[], PaymentStrategy]] = {
"stripe": lambda: StripeStrategy(api_key=os.getenv("STRIPE_KEY")),
"paypal": lambda: PayPalStrategy(client_id=os.getenv("PAYPAL_ID"), secret=os.getenv("PAYPAL_SECRET")),
"braintree": lambda: BraintreeStrategy(token=os.getenv("BT_TOKEN")),
}
This approach makes the system fully configurable without any code changes — the registry is the bridge between configuration strings and strategy objects.
Key Takeaways
🎯 Core Concept: The Strategy Pattern defines a family of interchangeable algorithms, encapsulates each one in its own class behind a shared interface, and lets the caller inject the desired algorithm into the context at runtime. The context delegates all algorithmic work to the injected strategy and never contains any algorithm logic itself. The pattern transforms an if/else-driven algorithm selector into a clean, extensible object hierarchy where adding a new algorithm is adding a new class — not editing existing code.
🔑 Key Benefits:
- Open/Closed Principle: New algorithms extend the system without modifying the context or existing strategies
- Single Responsibility: Each strategy class owns exactly one algorithm and nothing else
- Runtime swappability: Algorithms can be changed at any point in the object's lifetime based on user preference, data characteristics, or environment
- Isolated testability: Each strategy is a pure unit — test it with inputs and assert outputs, no context setup required
- Conditional elimination: The context is free of
if/switchalgorithm selection — it simply delegates
🌐 Language-Specific Highlights:
- Python: Use
ABC+@abstractmethodfor the strategy interface; prefer callables (Callable[[T], R]) for stateless single-method strategies — Python functions ARE first-class strategies; use module-level singletons for stateless strategies - TypeScript: Use an interface, not a class or abstract class, for the strategy contract; use
type StrategyFn = (input: T) => Rfor single-method stateless strategies; keep concrete strategy classes unexported - Java: Always use an interface (not abstract class) for the strategy — abstract classes with no-op methods create silent bugs; use
Optional<String>for validation errors; userecordfor immutable value objects alongside strategies - JavaScript: Use
#privatefields for the strategy property — prevents external strategy injection; always copy input arrays before sorting — strategies should be pure; module-levelconstobjects work as singleton strategies - C#: Keep concrete strategies
internal sealed— consumers depend on the interface; useFunc<T, R>for stateless single-method strategies; use a static factory class to expose strategies without leaking concrete types - PHP: Use PHP 8.0+ constructor property promotion for clean strategy constructors; use
readonlyproperties for immutable strategy configuration;matchexpressions complement (not replace) the pattern for small closed sets - Go: Use a
type SortFunc func([]int) []intfor single-function strategies — idiomatic Go; use interfaces for multi-method strategies; always check for nil strategy before delegating; initialize with a default strategy in the constructor - Rust: Use
Box<dyn Trait>for the strategy field; deriveDebugandClonewhere possible; use the struct-per-strategy approach instead of enums for open extensibility; avoid closures that capture too much state - Dart: Use
typedeffor function strategies —typedef StrategyFn = ReturnType Function(ArgType)is idiomatic; useabstract classfor multi-method strategies; Dart 3sealed classfor closed, exhaustive strategy sets - Swift: Use
protocol(not class inheritance) for the strategy contract;structstrategies for value semantics;AnyObject-constrained protocols for class-based strategies that hold mutable state; closures work as function strategies - Kotlin: Use
fun interfacefor SAM conversion — pass lambdas directly as single-method strategies; useobjectfor stateless singleton strategies; usesealed interfacefor a closed, exhaustive set of known strategies withwhenexhaustiveness
📋 When to Use Strategy:
- An object needs to use one of several related algorithms that vary independently of the client code
- The variation involves distinct, non-trivial logic that warrants its own class (not just a parameter difference)
- You want to swap algorithms at runtime based on user choice, configuration, or data characteristics
- You want to test each algorithm in complete isolation from the context
- You expect the family of algorithms to grow over time without touching the context
⚠️ When NOT to Use Strategy:
- Only one algorithm exists and will never change — strategy interface adds pure overhead
- The algorithms are trivially short (one line) — a lambda or direct call is simpler
- The variation is a parameter, not an algorithm (use a constructor argument)
- All variants are known at compile time in a small, sealed set — exhaustive pattern matching is cleaner
- Performance-critical hot paths where virtual dispatch overhead is measurable
🛠️ Best Practices Across Languages:
- Define a strict interface: Every method the context calls on the strategy must be in the interface — partial interfaces lead to casting and broken polymorphism
- Keep strategies stateless when possible: Pure strategies (same input → same output, no side effects) are trivially testable, safely sharable as singletons, and composable
- Inject strategies from outside: The context should never create its own strategy — inject via constructor or setter; this enables testing with mocks and DI framework wiring
- Never mutate the input: Strategies should operate on copies — mutating the caller's data is an unexpected side effect that causes hard-to-find bugs
- Use a factory for strategy selection logic: When the strategy depends on data characteristics or configuration, extract the selection logic into a dedicated factory or selector — keep the context and strategies free of selection logic
- Keep concrete strategy classes non-public: Expose only the interface; consumers should never couple to a specific concrete strategy class
- Prefer composition over subclassing: Wrap strategies to add caching, logging, or retry —
CachingStrategy(QuickSortStrategy())— rather than creating subclasses
💡 Common Pitfalls:
- String-based algorithm selection inside the context: Replacing objects with string flags (
if method == "quick") defeats the pattern — the context is still responsible for every algorithm - Instantiating strategies inside the context: The context should receive strategies from outside — constructing them internally couples the context to every concrete strategy
- Abstract base class instead of interface: An abstract class with empty method bodies creates silent no-op bugs when a concrete strategy forgets to override a method — use an interface so the compiler enforces all methods
- Mutating strategy input: Strategies that modify their argument arrays corrupt the caller's data — always work on copies
- Putting selection logic back in the context: A context that inspects data and chooses its own strategy has re-absorbed the responsibility that Strategy was meant to extract — put selection in a factory
- Overly granular strategies: Creating a strategy class for a one-liner algorithm is over-engineering — use a lambda or direct call for trivial logic
🎁 Advanced Techniques:
- Composable strategies (Decorator + Strategy): Wrap a strategy in another strategy to add cross-cutting concerns:
CachedStrategy(LoggedStrategy(QuickSortStrategy()))— each wrapper adds one capability without modifying the inner algorithm - Strategy Registry: A
Map<String, Callable<Strategy>>maps configuration strings or user choices to strategy factory functions — enables fully external strategy configuration without code changes - Adaptive Strategy Selector: A meta-strategy that benchmarks multiple strategies on a sample of the input and switches to the fastest one — self-tuning data pipelines use this for automatic algorithm selection
- Async Strategies: In JavaScript/TypeScript, strategies can return
Promise<T>— anAsyncPaymentStrategythat awaits an API call is structurally identical to a synchronous one; the context just awaits the result - Strategy + Observer: Notify observers when the strategy changes —
onStrategyChanged(old, new)— useful for logging, analytics, and UI updates when a user switches payment methods or routing preferences - Null Object Strategy: A
NoOpStrategyorPassThroughStrategythat does nothing (or returns the input unchanged) — a safe default that eliminates null checks in the context and enables gradual feature rollout
The Strategy Pattern is the cleanest solution whenever you have a family of algorithms that needs to be independently developed, tested, and swapped. It is the first pattern to reach for when you find yourself writing if (method === "A") { ... } else if (method === "B") { ... } for anything more complex than a trivial configuration switch. The key insight is simple and powerful: algorithms are objects — they deserve their own classes, their own tests, and their own identities.
Want to dive deeper into design patterns? Check out our comprehensive guides on:
- Memento Pattern
- Observer Pattern
- State Pattern
- Command Pattern
- Template Method Pattern Each guide includes examples in all 11 programming languages with language-specific best practices.