Strategy Pattern: Interchangeable Algorithms at Runtime
design-patternsbehavioralstrategy

What is the Strategy Pattern?
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the algorithm to vary independently of the clients that use it.
classDiagram
class Context {
-strategy: Strategy
+setStrategy(strategy)
+executeStrategy()
}
class Strategy {
<<interface>>
+execute(data)
}
class ConcreteStrategyA {
+execute(data)
}
class ConcreteStrategyB {
+execute(data)
}
Context o--> Strategy
Strategy <|.. ConcreteStrategyA
Strategy <|.. ConcreteStrategyBIn simple terms: instead of having a giant if-else to decide which algorithm to use, you extract each algorithm to its own class and swap them as needed.
The Problem It Solves
class PaymentProcessor {
process(method: string, amount: number): void {
if (method === 'credit_card') {
this.processCreditCard(amount);
} else if (method === 'paypal') {
this.processPayPal(amount);
} else if (method === 'crypto') {
this.processCrypto(amount);
}
}
}Problems: Class grows out of control, hard to test, violates Open/Closed.
The Solution: Strategy
1. Define the Strategy Interface
interface PaymentData {
amount: number;
currency: string;
customerId: string;
}
interface PaymentResult {
success: boolean;
transactionId: string;
fee: number;
netAmount: number;
}
/** Contract that every payment strategy must implement */
interface PaymentStrategy {
readonly name: string;
process(data: PaymentData): Promise<PaymentResult>;
calculateFee(amount: number): number;
validate(data: PaymentData): boolean;
}2. Implement Concrete Strategies
/** Strategy for credit card payments */
class CreditCardStrategy implements PaymentStrategy {
readonly name = 'credit_card';
async process(data: PaymentData): Promise<PaymentResult> {
const fee = this.calculateFee(data.amount);
return {
success: true,
transactionId: `cc_${Date.now()}`,
fee,
netAmount: data.amount - fee,
};
}
calculateFee(amount: number): number {
return amount * 0.029 + 0.30;
}
validate(data: PaymentData): boolean {
return data.amount <= 10000;
}
}
/** Strategy for cryptocurrency payments */
class CryptoStrategy implements PaymentStrategy {
readonly name = 'crypto';
async process(data: PaymentData): Promise<PaymentResult> {
const fee = this.calculateFee(data.amount);
return {
success: true,
transactionId: `crypto_${Date.now()}`,
fee,
netAmount: data.amount - fee,
};
}
calculateFee(amount: number): number {
return amount * 0.01;
}
validate(data: PaymentData): boolean {
return data.amount >= 10;
}
}3. Create the Context
/** Processor that accepts multiple payment strategies */
class PaymentProcessor {
private strategies = new Map<string, PaymentStrategy>();
private currentStrategy: PaymentStrategy | null = null;
registerStrategy(strategy: PaymentStrategy): this {
this.strategies.set(strategy.name, strategy);
return this;
}
setStrategy(name: string): this {
const strategy = this.strategies.get(name);
if (!strategy) throw new Error(`Unknown strategy: ${name}`);
this.currentStrategy = strategy;
return this;
}
async process(data: PaymentData): Promise<PaymentResult> {
if (!this.currentStrategy) throw new Error('No strategy selected');
if (!this.currentStrategy.validate(data)) throw new Error('Validation failed');
return this.currentStrategy.process(data);
}
/** Automatically selects the strategy with lowest fee */
selectBestStrategy(amount: number): string {
let bestStrategy: PaymentStrategy | null = null;
let lowestFee = Infinity;
for (const strategy of this.strategies.values()) {
if (strategy.validate({ amount, currency: 'USD', customerId: '' })) {
const fee = strategy.calculateFee(amount);
if (fee < lowestFee) {
lowestFee = fee;
bestStrategy = strategy;
}
}
}
if (!bestStrategy) throw new Error('No valid strategy');
this.currentStrategy = bestStrategy;
return bestStrategy.name;
}
}Usage Example
const processor = new PaymentProcessor()
.registerStrategy(new CreditCardStrategy())
.registerStrategy(new CryptoStrategy());
processor.setStrategy('crypto');
const result = await processor.process({
amount: 99.99,
currency: 'USD',
customerId: 'cust_123',
});
const bestMethod = processor.selectBestStrategy(500);
console.log(`Best method for $500: ${bestMethod}`);Benefits of the Strategy Pattern
| Aspect | Benefit |
|---|---|
| Open/Closed | Add strategies without modifying code |
| Single Responsibility | Each strategy has its own class |
| Testability | Each strategy can be tested in isolation |
| Flexibility | Change algorithm at runtime |
When to Use Strategy
- When you need different variants of an algorithm
- When you want to avoid multiple conditionals
- When you have classes that only differ in behavior
When NOT to Use Strategy
- If there are only 2-3 simple algorithms that will never change
- If algorithm selection doesn't change at runtime
Conclusion
The Strategy pattern is a powerful tool for handling multiple algorithms cleanly and extensibly.
In the next article we'll explore the Adapter Pattern, which allows us to integrate incompatible interfaces.
Based on "Design Patterns: Elements of Reusable Object-Oriented Software" (Gang of Four).