Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable. A behavioral design pattern for flexible behavior selection.
Overview
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one as a separate class, and makes them interchangeable at runtime. It lets the algorithm vary independently from the clients that use it.
It is ideal when you need to select from multiple ways to perform an operation, such as different sorting strategies, payment methods, or compression algorithms.
When to Use
Use the Strategy Pattern when:
- You have multiple ways to perform an operation and want to switch between them at runtime
- A class has a large conditional (
if/switch) for selecting behavior - You want to isolate algorithm implementation details from the context that uses them
- You need to add new algorithms without modifying existing code (Open/Closed Principle)
- Different clients need different variants of the same algorithm
Solution
Python
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
pass
class CreditCardPayment(PaymentStrategy):
def pay(self, amount: float) -> str:
return f"Paid ${amount} with Credit Card"
class PayPalPayment(PaymentStrategy):
def pay(self, amount: float) -> str:
return f"Paid ${amount} via PayPal"
class Checkout:
def __init__(self, strategy: PaymentStrategy):
self.strategy = strategy
def process(self, amount: float):
return self.strategy.pay(amount)
# Usage
checkout = Checkout(CreditCardPayment())
print(checkout.process(100.0))
checkout.strategy = PayPalPayment()
print(checkout.process(50.0))
JavaScript
class CreditCardPayment {
pay(amount) {
return `Paid $${amount} with Credit Card`;
}
}
class PayPalPayment {
pay(amount) {
return `Paid $${amount} via PayPal`;
}
}
class Checkout {
constructor(strategy) {
this.strategy = strategy;
}
process(amount) {
return this.strategy.pay(amount);
}
}
// Usage
const checkout = new Checkout(new CreditCardPayment());
console.log(checkout.process(100));
checkout.strategy = new PayPalPayment();
console.log(checkout.process(50));
Java
interface PaymentStrategy {
String pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
public String pay(double amount) {
return "Paid $" + amount + " with Credit Card";
}
}
class PayPalPayment implements PaymentStrategy {
public String pay(double amount) {
return "Paid $" + amount + " via PayPal";
}
}
class Checkout {
private PaymentStrategy strategy;
Checkout(PaymentStrategy strategy) {
this.strategy = strategy;
}
String process(double amount) {
return strategy.pay(amount);
}
void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
}
// Usage
Checkout checkout = new Checkout(new CreditCardPayment());
System.out.println(checkout.process(100));
checkout.setStrategy(new PayPalPayment());
System.out.println(checkout.process(50));
Explanation
The Strategy Pattern separates behavior into three roles:
- Strategy Interface (
PaymentStrategy): Defines the contract all algorithms must follow - Concrete Strategies (
CreditCardPayment,PayPalPayment): The actual interchangeable algorithms - Context (
Checkout): Uses a strategy through the interface, unaware of the concrete implementation
This eliminates large conditional blocks and makes adding new strategies as simple as creating a new class.
Variants
| Variant | Use Case | Trade-off |
|---|---|---|
| Simple Strategy | Single context, few strategies | Easy to start, direct coupling |
| Strategy with Factory | Dynamic strategy selection | More flexible, adds indirection |
| Functional Strategy | Languages with first-class functions | Concise, but less explicit than classes |
What Works
- Use an interface or abstract base to ensure all strategies have the same signature
- Keep strategies stateless when possible for easier reuse and testing
- Inject the strategy via constructor or setter rather than hardcoding it
- Document strategy differences so callers know which to choose
- Avoid strategy bloat: if you have dozens of strategies, consider a different abstraction
Common Mistakes
- Leaking context internals: Strategies should not depend on private details of the context
- Overuse for trivial cases: A simple
ifstatement does not always need a Strategy class - Tight coupling: The context knowing about concrete strategy classes instead of the interface
- Inconsistent interfaces: Strategies with different method signatures break interchangeability
- State management: Strategies holding mutable state can cause unexpected side effects
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward.
- Tests break when the design changes: favor stable contracts over internal structure.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Advanced Topics
Scenario: Strategy for Pricing Algorithms
// Strategy pattern: swap algorithms at runtime
interface PricingStrategy {
calculate(basePrice: number, context: PricingContext): number;
}
interface PricingContext {
userAge?: number;
isMember?: boolean;
orderCount?: number;
isHoliday?: boolean;
}
class RegularPricing implements PricingStrategy {
calculate(basePrice: number): number { return basePrice; }
}
class MemberDiscountPricing implements PricingStrategy {
calculate(basePrice: number, ctx: PricingContext): number {
const discount = ctx.orderCount > 10 ? 0.20 : 0.10;
return basePrice * (1 - discount);
}
}
class SeniorDiscountPricing implements PricingStrategy {
calculate(basePrice: number, ctx: PricingContext): number {
if (ctx.userAge >= 65) return basePrice * 0.85;
return basePrice;
}
}
class HolidayPricing implements PricingStrategy {
calculate(basePrice: number, ctx: PricingContext): number {
if (ctx.isHoliday) return basePrice * 0.90;
return basePrice;
}
}
// Context: uses the active strategy
class PricingService {
private strategy: PricingStrategy;
constructor(strategy: PricingStrategy) {
this.strategy = strategy;
}
setStrategy(strategy: PricingStrategy) {
this.strategy = strategy;
}
calculatePrice(basePrice: number, ctx: PricingContext): number {
return this.strategy.calculate(basePrice, ctx);
}
}
// Usage: switch strategy at runtime
const pricing = new PricingService(new RegularPricing());
// Regular customer
console.log(pricing.calculatePrice(100, {})); // 100
// Switch to member discount
pricing.setStrategy(new MemberDiscountPricing());
console.log(pricing.calculatePrice(100, { orderCount: 15 })); // 80
// Switch to senior
pricing.setStrategy(new SeniorDiscountPricing());
console.log(pricing.calculatePrice(100, { userAge: 70 })); // 85
// Combine strategies (composite)
class CompositePricing implements PricingStrategy {
constructor(private strategies: PricingStrategy[]) {}
calculate(basePrice: number, ctx: PricingContext): number {
return this.strategies.reduce(
(price, s) => s.calculate(price, ctx),
basePrice
);
}
}
// Apply member + holiday
const composite = new CompositePricing([
new MemberDiscountPricing(),
new HolidayPricing(),
]);
console.log(composite.calculate(100, { orderCount: 15, isHoliday: true })); // 72
Lessons:
- Strategy swaps algorithms without touching the client
- Each strategy is a separate class (Open/Closed)
- Composite strategy combines multiple discounts
- The client does not know algorithm details
- Strategy vs State: Strategy changes algorithm, State changes behavior based on state
### Strategy vs State: which do I use?
Strategy changes the algorithm from outside: the client chooses which strategy to use. State changes behavior from inside: the object changes its behavior based on its internal state. Strategy is explicit: pricing.setStrategy(new MemberDiscount()). State is implicit: the object decides based on its state. Strategy is a behavior pattern; State is a state pattern. Frequently Asked Questions
What is the difference between Strategy and State patterns?
Strategy is about interchangeable algorithms chosen by the client. State is about changing behavior based on the object's internal state transitions.
Can I use functions instead of classes for strategies?
Yes, in languages with first-class functions (JavaScript, Python, Go) you can pass functions directly. Classes are better when strategies need configuration or multiple methods.
How many strategies is too many?
There is no hard limit, but if you find yourself with dozens, consider grouping them by category or using a registry/factory pattern.
Related Resources
Observer Pattern
Define a subscription mechanism to notify multiple objects about events. A behavioral design pattern for event-driven communication.
RecipeSort an Array
How to sort arrays and lists in ascending, descending, and custom order across multiple languages.
GuideREST API Design Guide
A thorough guide to designing clean, scalable, and maintainable REST APIs.
PatternBlackboard Pattern
A shared knowledge space where independent specialized modules collaborate to solve complex problems by contributing partial solutions.
PatternBridge Pattern: Decouple Abstraction from Implementation
Split a class into two hierarchies — abstraction and implementation — so both can evolve independently. Includes Python, Java, and JavaScript examples.
PatternChain of Responsibility Pattern
Pass requests along a chain of handlers until one handles it. A behavioral design pattern for decoupling senders and receivers.