The Strategy pattern lets us define a family of algorithms, put each one in its own class, and swap them at runtime. The client code doesn’t change at all — it just picks a different strategy.
Think of it like a navigation app. We want to go from A to B. The app gives us options: fastest route, shortest route, avoid tolls, walking only. Same destination, same app — but completely different algorithms calculating the path. That’s Strategy.
The Problem
Imagine we’re building a payment system. Without Strategy, our code looks like this:
if paymentType == "credit_card":
# 30 lines of credit card logic
elif paymentType == "paypal":
# 25 lines of PayPal logic
elif paymentType == "crypto":
# 35 lines of crypto logic
elif paymentType == "bank_transfer":
# 20 lines of bank transfer logic
Every new payment method means another elif block. The class gets bigger and bigger. Testing is painful. Adding UPI means touching this giant file. This violates both Single Responsibility and Open/Closed principles.
How Strategy Fixes This
- pay(amount)
Each algorithm gets its own class. The context (PaymentProcessor) holds a reference to a strategy and delegates to it. We can swap strategies at runtime without touching any existing code.
Implementation
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def execute(self, amount: float) -> None:
pass
class CreditCardPayment(PaymentStrategy):
def __init__(self, card_number: str):
self.card_number = card_number
def execute(self, amount: float):
print(f"Charged ${amount:.2f} to card ending {self.card_number[-4:]}")
class PayPalPayment(PaymentStrategy):
def __init__(self, email: str):
self.email = email
def execute(self, amount: float):
print(f"Sent ${amount:.2f} via PayPal to {self.email}")
class CryptoPayment(PaymentStrategy):
def __init__(self, wallet_address: str):
self.wallet = wallet_address
def execute(self, amount: float):
print(f"Transferred ${amount:.2f} in crypto to {self.wallet[:8]}...")
class PaymentProcessor:
def __init__(self, strategy: PaymentStrategy):
self._strategy = strategy
def set_strategy(self, strategy: PaymentStrategy):
self._strategy = strategy
def pay(self, amount: float):
self._strategy.execute(amount)
# Usage
processor = PaymentProcessor(CreditCardPayment("4111111111111234"))
processor.pay(99.99) # Charged $99.99 to card ending 1234
processor.set_strategy(PayPalPayment("dev@example.com"))
processor.pay(49.99) # Sent $49.99 via PayPal to dev@example.com
processor.set_strategy(CryptoPayment("0xABCDEF1234567890"))
processor.pay(199.99) # Transferred $199.99 in crypto to 0xABCDEF...
class CreditCardPayment {
constructor(cardNumber) {
this.cardNumber = cardNumber;
}
execute(amount) {
console.log(`Charged $${amount.toFixed(2)} to card ending ${this.cardNumber.slice(-4)}`);
}
}
class PayPalPayment {
constructor(email) {
this.email = email;
}
execute(amount) {
console.log(`Sent $${amount.toFixed(2)} via PayPal to ${this.email}`);
}
}
class CryptoPayment {
constructor(walletAddress) {
this.wallet = walletAddress;
}
execute(amount) {
console.log(`Transferred $${amount.toFixed(2)} in crypto to ${this.wallet.slice(0, 8)}...`);
}
}
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
pay(amount) {
this.strategy.execute(amount);
}
}
// Usage
const processor = new PaymentProcessor(new CreditCardPayment("4111111111111234"));
processor.pay(99.99);
processor.setStrategy(new PayPalPayment("dev@example.com"));
processor.pay(49.99);
interface PaymentStrategy {
void execute(double amount);
}
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
public void execute(double amount) {
System.out.printf("Charged $%.2f to card ending %s%n",
amount, cardNumber.substring(cardNumber.length() - 4));
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) { this.email = email; }
public void execute(double amount) {
System.out.printf("Sent $%.2f via PayPal to %s%n", amount, email);
}
}
class CryptoPayment implements PaymentStrategy {
private String wallet;
public CryptoPayment(String wallet) { this.wallet = wallet; }
public void execute(double amount) {
System.out.printf("Transferred $%.2f in crypto to %s...%n",
amount, wallet.substring(0, 8));
}
}
class PaymentProcessor {
private PaymentStrategy strategy;
public PaymentProcessor(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void pay(double amount) {
strategy.execute(amount);
}
}
When to Use
- Multiple algorithms for the same task — sorting, compression, validation, pricing
- Eliminating conditional logic — replacing if-else chains with polymorphism
- Runtime behavior switching — user picks a shipping method, discount type, etc.
- Testing — we can inject mock strategies easily
When NOT to Use
- When we only have 2 algorithms that will never change — a simple if-else is fine
- When the algorithms share a lot of state with the context — the separation becomes awkward
- When clients don’t need to know about different strategies — added complexity for no gain
Strategy vs State Pattern
People mix these up constantly in interviews. The key difference:
- Strategy — the client picks which algorithm to use. Strategies don’t know about each other.
- State — the object transitions between states on its own. States know which state comes next.
The only difference is who controls the switching. In Strategy, it’s the outside code. In State, it’s the states themselves.
In simple language, Strategy is like having a toolbox. We pick the right tool for the job and snap it in. Need a different tool? Swap it out. The workbench (context) doesn’t care which tool we’re using — it just says “do the thing” and the tool handles the rest.