Strategy Pattern: Swap Algorithms at Runtime
TL;DR
The Strategy Pattern defines a family of interchangeable algorithms, encapsulates each one, and makes them interchangeable at runtime. Instead of hardcoding behavior into a class, you inject the algorithm as an object, allowing you to swap strategies without changing the client code.
Core Concept
What is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that enables you to define a family of algorithms, put each one in a separate class, and make their objects interchangeable. The pattern extracts varying behavior into separate classes that all implement a common interface.
Why Use Strategy Pattern?
Without Strategy Pattern, you often end up with large conditional statements (if-else or switch) that select behavior based on some flag or type. This violates the Open/Closed Principle — every time you add a new behavior, you must modify existing code.
Strategy Pattern solves this by:
- Eliminating conditionals: Replace complex if-else chains with polymorphism
- Enabling runtime flexibility: Swap algorithms dynamically based on context
- Improving testability: Test each strategy in isolation
- Following Open/Closed: Add new strategies without touching existing code
Core Components
- Strategy Interface: Defines the contract all concrete strategies must follow
- Concrete Strategies: Implement the strategy interface with specific algorithms
- Context: Maintains a reference to a strategy object and delegates work to it
When to Use
Use Strategy Pattern when:
- You have multiple ways to perform an operation and want to choose at runtime
- You want to avoid exposing complex, algorithm-specific data structures
- A class has multiple conditional statements that select different behaviors
- You need different variants of an algorithm (e.g., sorting, compression, validation)
Real-World Analogy
Think of navigation apps. You can choose different routing strategies: fastest route, shortest distance, avoid highways, or scenic route. The app (context) doesn’t change, but the algorithm (strategy) for calculating the route does. You select your preferred strategy, and the app uses it to compute directions.
Visual Guide
Strategy Pattern Structure
classDiagram
class Context {
-strategy: Strategy
+setStrategy(Strategy)
+executeStrategy()
}
class Strategy {
<<interface>>
+execute()*
}
class ConcreteStrategyA {
+execute()
}
class ConcreteStrategyB {
+execute()
}
class ConcreteStrategyC {
+execute()
}
Context o-- Strategy
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyB
Strategy <|.. ConcreteStrategyC
The Context holds a reference to a Strategy interface. Concrete strategies implement the interface with different algorithms. The context can switch strategies at runtime.
Strategy Pattern Flow
sequenceDiagram
participant Client
participant Context
participant Strategy
Client->>Context: create with StrategyA
Client->>Context: executeStrategy()
Context->>Strategy: execute()
Strategy-->>Context: result
Context-->>Client: result
Client->>Context: setStrategy(StrategyB)
Client->>Context: executeStrategy()
Context->>Strategy: execute()
Strategy-->>Context: result
Context-->>Client: result
Client creates context with initial strategy, executes it, then swaps to a different strategy and executes again — all without changing the context’s code.
Examples
Example 1: Payment Processing System
Let’s build a payment system that supports multiple payment methods. Without Strategy Pattern, you’d have messy conditionals. With it, each payment method is a separate strategy.
from abc import ABC, abstractmethod
# Strategy Interface
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
pass
# Concrete Strategies
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str, cvv: str):
self.card_number = card_number
self.cvv = cvv
def pay(self, amount: float) -> str:
# Simulate payment processing
return f"Paid ${amount:.2f} using Credit Card ending in {self.card_number[-4:]}"
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} using PayPal account {self.email}"
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self.wallet_address = wallet_address
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} using Crypto wallet {self.wallet_address[:8]}..."
# Context
class ShoppingCart:
def __init__(self):
self.items = []
self.payment_strategy = None
def add_item(self, item: str, price: float):
self.items.append((item, price))
def set_payment_strategy(self, strategy: PaymentStrategy):
self.payment_strategy = strategy
def checkout(self) -> str:
if not self.payment_strategy:
return "Error: No payment method selected"
total = sum(price for _, price in self.items)
return self.payment_strategy.pay(total)
# Client code
cart = ShoppingCart()
cart.add_item("Laptop", 999.99)
cart.add_item("Mouse", 29.99)
# Use credit card
cart.set_payment_strategy(CreditCardPayment("1234-5678-9012-3456", "123"))
print(cart.checkout())
# Output: Paid $1029.98 using Credit Card ending in 3456
# Switch to PayPal
cart.set_payment_strategy(PayPalPayment("user@example.com"))
print(cart.checkout())
# Output: Paid $1029.98 using PayPal account user@example.com
# Switch to Crypto
cart.set_payment_strategy(CryptoPayment("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"))
print(cart.checkout())
# Output: Paid $1029.98 using Crypto wallet 0x742d35...
Key Points:
- The
ShoppingCartdoesn’t know how payment works — it just callspay() - Adding a new payment method (e.g., Apple Pay) requires creating a new strategy class, not modifying existing code
- Each strategy can have its own state (card number, email, wallet address)
Try it yourself: Add a BankTransferPayment strategy that requires account number and routing number.
Example 2: Data Compression
Different compression algorithms for different scenarios — speed vs. compression ratio.
from abc import ABC, abstractmethod
import time
# Strategy Interface
class CompressionStrategy(ABC):
@abstractmethod
def compress(self, data: str) -> dict:
"""Returns dict with compressed data and metadata"""
pass
# Concrete Strategies
class ZipCompression(CompressionStrategy):
def compress(self, data: str) -> dict:
# Simulate ZIP compression (balanced speed/ratio)
time.sleep(0.1) # Simulate processing
compressed_size = len(data) * 0.6 # 40% compression
return {
"algorithm": "ZIP",
"original_size": len(data),
"compressed_size": int(compressed_size),
"ratio": f"{(1 - 0.6) * 100:.0f}%",
"time": "0.1s"
}
class RarCompression(CompressionStrategy):
def compress(self, data: str) -> dict:
# Simulate RAR compression (better ratio, slower)
time.sleep(0.3) # Simulate processing
compressed_size = len(data) * 0.4 # 60% compression
return {
"algorithm": "RAR",
"original_size": len(data),
"compressed_size": int(compressed_size),
"ratio": f"{(1 - 0.4) * 100:.0f}%",
"time": "0.3s"
}
class FastCompression(CompressionStrategy):
def compress(self, data: str) -> dict:
# Simulate fast compression (speed priority)
time.sleep(0.05) # Simulate processing
compressed_size = len(data) * 0.8 # 20% compression
return {
"algorithm": "FAST",
"original_size": len(data),
"compressed_size": int(compressed_size),
"ratio": f"{(1 - 0.8) * 100:.0f}%",
"time": "0.05s"
}
# Context
class FileCompressor:
def __init__(self, strategy: CompressionStrategy):
self.strategy = strategy
def set_strategy(self, strategy: CompressionStrategy):
self.strategy = strategy
def compress_file(self, filename: str, data: str) -> str:
result = self.strategy.compress(data)
return (
f"File: {filename}\n"
f"Algorithm: {result['algorithm']}\n"
f"Original: {result['original_size']} bytes\n"
f"Compressed: {result['compressed_size']} bytes\n"
f"Compression: {result['ratio']}\n"
f"Time: {result['time']}"
)
# Client code
data = "A" * 1000 # 1000 bytes of data
# Start with ZIP
compressor = FileCompressor(ZipCompression())
print(compressor.compress_file("document.txt", data))
print()
# Output:
# File: document.txt
# Algorithm: ZIP
# Original: 1000 bytes
# Compressed: 600 bytes
# Compression: 40%
# Time: 0.1s
# Switch to RAR for better compression
compressor.set_strategy(RarCompression())
print(compressor.compress_file("document.txt", data))
print()
# Output:
# File: document.txt
# Algorithm: RAR
# Original: 1000 bytes
# Compressed: 400 bytes
# Compression: 60%
# Time: 0.3s
# Switch to Fast for speed
compressor.set_strategy(FastCompression())
print(compressor.compress_file("document.txt", data))
# Output:
# File: document.txt
# Algorithm: FAST
# Original: 1000 bytes
# Compressed: 800 bytes
# Compression: 20%
# Time: 0.05s
Key Points:
- Each strategy encapsulates a different compression algorithm
- The context can switch strategies based on user preference or file type
- Strategies can return complex data structures, not just simple values
Try it yourself: Add a NoCompression strategy that just returns the original data unchanged.
Java/C++ Notes
Java:
// Strategy interface
interface PaymentStrategy {
String pay(double amount);
}
// Concrete strategy
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public String pay(double amount) {
return String.format("Paid $%.2f using Credit Card", amount);
}
}
// Context
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public String checkout(double amount) {
return paymentStrategy.pay(amount);
}
}
C++:
// Strategy interface
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual std::string pay(double amount) = 0;
};
// Concrete strategy
class CreditCardPayment : public PaymentStrategy {
private:
std::string cardNumber;
public:
CreditCardPayment(const std::string& card) : cardNumber(card) {}
std::string pay(double amount) override {
return "Paid $" + std::to_string(amount) + " using Credit Card";
}
};
// Context
class ShoppingCart {
private:
std::unique_ptr<PaymentStrategy> paymentStrategy;
public:
void setPaymentStrategy(std::unique_ptr<PaymentStrategy> strategy) {
paymentStrategy = std::move(strategy);
}
std::string checkout(double amount) {
return paymentStrategy->pay(amount);
}
};
Key difference: C++ uses smart pointers (unique_ptr) to manage strategy lifetime and prevent memory leaks.
Common Mistakes
1. Creating Too Many Strategies
Mistake: Creating a separate strategy class for every tiny variation in behavior.
# BAD: Over-engineering
class DiscountStrategy5Percent(DiscountStrategy):
def calculate(self, amount):
return amount * 0.95
class DiscountStrategy10Percent(DiscountStrategy):
def calculate(self, amount):
return amount * 0.90
class DiscountStrategy15Percent(DiscountStrategy):
def calculate(self, amount):
return amount * 0.85
Better: Use a parameterized strategy when the algorithm is the same but values differ.
# GOOD: Single parameterized strategy
class PercentageDiscount(DiscountStrategy):
def __init__(self, percentage: float):
self.percentage = percentage
def calculate(self, amount: float) -> float:
return amount * (1 - self.percentage / 100)
# Usage
discount5 = PercentageDiscount(5)
discount10 = PercentageDiscount(10)
2. Exposing Strategy Internals to Context
Mistake: The context knows too much about how strategies work internally.
# BAD: Context knows strategy implementation details
class Context:
def execute(self):
if isinstance(self.strategy, ComplexStrategy):
# Special handling for ComplexStrategy
self.strategy.setup()
result = self.strategy.execute()
self.strategy.cleanup()
else:
result = self.strategy.execute()
return result
Better: All strategies should have the same interface. Handle setup/cleanup within the strategy.
# GOOD: Uniform interface
class ComplexStrategy(Strategy):
def execute(self):
self._setup()
result = self._do_work()
self._cleanup()
return result
def _setup(self):
# Internal setup
pass
class Context:
def execute(self):
return self.strategy.execute() # Same for all strategies
3. Not Using Dependency Injection
Mistake: Hardcoding strategy creation inside the context.
# BAD: Context creates its own strategy
class Context:
def __init__(self, strategy_type: str):
if strategy_type == "fast":
self.strategy = FastStrategy()
elif strategy_type == "slow":
self.strategy = SlowStrategy()
# Now you're back to conditionals!
Better: Inject the strategy from outside.
# GOOD: Strategy injected via constructor or setter
class Context:
def __init__(self, strategy: Strategy):
self.strategy = strategy
# Client decides which strategy
context = Context(FastStrategy())
4. Forgetting to Handle Missing Strategy
Mistake: Not checking if a strategy has been set before using it.
# BAD: Assumes strategy is always set
class Context:
def __init__(self):
self.strategy = None
def execute(self):
return self.strategy.execute() # Crashes if None!
Better: Validate strategy existence or provide a default.
# GOOD: Validate or use default
class Context:
def __init__(self, strategy: Strategy = None):
self.strategy = strategy or DefaultStrategy()
def execute(self):
if not self.strategy:
raise ValueError("No strategy set")
return self.strategy.execute()
5. Confusing Strategy with State Pattern
Mistake: Using Strategy Pattern when State Pattern is more appropriate.
Strategy Pattern: Client chooses which algorithm to use. Strategies don’t know about each other.
State Pattern: Object changes its behavior when its internal state changes. States may transition to other states.
If your “strategies” are transitioning between each other based on internal logic, you probably want State Pattern instead.
Interview Tips
What Interviewers Look For
1. Recognition of the Pattern
Interviewers often present a problem with multiple algorithms and ask how you’d design it. They’re testing if you recognize this as a Strategy Pattern opportunity.
Red flag answer: “I’d use a big if-else statement to choose the algorithm.”
Strong answer: “This is a perfect use case for Strategy Pattern. I’d define a strategy interface, create concrete strategies for each algorithm, and inject the appropriate strategy into the context.”
2. Articulating Benefits
Be ready to explain WHY Strategy Pattern is better than conditionals:
- Open/Closed Principle: Add new strategies without modifying existing code
- Single Responsibility: Each strategy focuses on one algorithm
- Testability: Test each strategy independently
- Runtime flexibility: Swap algorithms dynamically
3. Coding Under Pressure
Practice implementing Strategy Pattern quickly. Interviewers may ask you to:
- Design a sorting system with multiple algorithms (bubble, quick, merge)
- Build a validation system with different rules
- Create a pricing calculator with various discount strategies
Template to memorize:
# 1. Define interface
class Strategy(ABC):
@abstractmethod
def execute(self, data):
pass
# 2. Create concrete strategies
class ConcreteStrategyA(Strategy):
def execute(self, data):
# Implementation A
pass
# 3. Build context
class Context:
def __init__(self, strategy: Strategy):
self.strategy = strategy
def do_work(self, data):
return self.strategy.execute(data)
4. Discussing Trade-offs
Interviewers want to know you understand when NOT to use Strategy Pattern:
Don’t use if:
- You only have 1-2 algorithms and they’re unlikely to change
- The algorithms are very simple (e.g., just a formula)
- Performance is critical and the overhead of polymorphism matters
Do use if:
- You have 3+ algorithms or expect to add more
- Algorithms are complex and deserve their own classes
- You need to swap algorithms at runtime
- You want to test algorithms independently
5. Real-World Examples
Have 2-3 real-world examples ready:
- Payment processing: Credit card, PayPal, crypto, bank transfer
- Sorting: Different algorithms based on data size/type
- Compression: ZIP, RAR, GZIP with different speed/ratio trade-offs
- Routing: Fastest, shortest, scenic, avoid highways
- Validation: Email, phone, credit card, password strength
6. Follow-up Questions
Be prepared for:
- “How is this different from Factory Pattern?” (Factory creates objects; Strategy defines behavior)
- “Could you use lambdas/functions instead?” (Yes, in Python/JavaScript, but classes provide better encapsulation for complex strategies)
- “How would you add a new strategy?” (Create new class implementing interface; no changes to existing code)
7. Code Review Scenario
Interviewers may show you code with conditionals and ask you to refactor it using Strategy Pattern. Practice identifying:
- The varying behavior (becomes strategies)
- The stable context (becomes context class)
- The common interface (becomes strategy interface)
Key Takeaways
- Strategy Pattern defines a family of interchangeable algorithms that can be selected and swapped at runtime without changing client code
- Use Strategy Pattern to eliminate conditional logic — replace if-else chains with polymorphism by creating separate strategy classes
- Follow the Open/Closed Principle — add new strategies by creating new classes, not by modifying existing code
- Inject strategies via constructor or setter — the context should receive strategies from outside, not create them internally
- Each strategy encapsulates one algorithm — strategies should be independent and not know about each other, making them easy to test in isolation