The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Difficulty: ⭐ Easy
Category: Behavioral Pattern
Interview Frequency: ⭐⭐⭐⭐ Very Common
Imagine you have different ways to perform the same task (e.g., different payment methods, sorting algorithms, compression algorithms). Without the Strategy pattern, you might:
- Use massive if-else or switch statements
- Create tightly coupled code
- Violate Open/Closed Principle (need to modify code to add new strategies)
Think of traveling from point A to B:
- By Car: Fast but expensive
- By Bus: Slower but cheaper
- By Walking: Free but slowest
All strategies achieve the same goal (travel), but the implementation differs. You choose based on your needs.
✅ Use Strategy Pattern when:
- You have multiple ways to perform a task
- You want to avoid conditionals for selecting behavior
- You need runtime algorithm switching
- Algorithms have different data structures
❌ Don't use when:
- You only have 2-3 simple variations (overkill)
- Strategies don't change at runtime
- The algorithm is simple and unlikely to change
See strategy.ts for the complete implementation.
- Strategy Interface: Defines the common interface for all algorithms
- Concrete Strategies: Implement the strategy interface with different algorithms
- Context: Maintains a reference to a strategy and delegates work to it
// Real-world example: Payment Processing System
interface PaymentStrategy {
pay(amount: number): void;
}
class CreditCardPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using Credit Card`);
}
}
class PayPalPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using PayPal`);
}
}
class ShoppingCart {
private strategy: PaymentStrategy;
setPaymentStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
checkout(amount: number): void {
this.strategy.pay(amount);
}
}✅ Open/Closed Principle - Add new strategies without modifying existing code
✅ Eliminates conditional statements
✅ Runtime flexibility
✅ Better testability
✅ Clear separation of concerns
❌ Increased number of classes
❌ Clients must be aware of different strategies
❌ Slight performance overhead (usually negligible)
Answer: Both look similar structurally, but:
- Strategy: Algorithms are independent; client chooses which to use
- State: States are interdependent; context changes state based on internal logic
- Strategy: Focuses on HOW to do something
- State: Focuses on WHAT state we're in
Answer: Yes! In languages with first-class functions (like TypeScript/JavaScript), you can pass functions instead of strategy objects:
type PaymentFunction = (amount: number) => void;
class ShoppingCart {
checkout(amount: number, paymentFn: PaymentFunction): void {
paymentFn(amount);
}
}
// Usage
cart.checkout(100, (amount) => console.log(`Paid ${amount} with Credit Card`));This is often simpler for simple cases, but Strategy pattern is better when:
- Strategies have state
- Strategies have multiple methods
- You need dependency injection
Answer: Simply create a new class implementing PaymentStrategy:
class CryptoPayment implements PaymentStrategy {
pay(amount: number): void {
console.log(`Paid ${amount} using Cryptocurrency`);
}
}No changes needed to ShoppingCart or other payment methods!
Answer:
- Only 2 simple alternatives → Use ternary or simple if-else
- Algorithm never changes → Hard-code it
- Performance is critical → Strategy adds indirection overhead
- Strategies are just data, not behavior → Use configuration
Answer: Several approaches:
- Common interface with optional parameters
- Strategy-specific setup methods
- Constructor injection
- Context provides all needed data
Example:
interface PaymentStrategy {
pay(amount: number, details?: any): void;
}
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
pay(amount: number): void {
// Use this.cardNumber and this.cvv
}
}- E-commerce: Payment methods (Credit Card, PayPal, Crypto)
- Sorting: Different algorithms based on data size (QuickSort, MergeSort, BubbleSort)
- Compression: ZIP, RAR, GZIP
- Routing: Car, Bicycle, Walking (Google Maps)
- Authentication: OAuth, JWT, Basic Auth
- Logging: Console, File, Remote server
- Validation: Email, Phone, Password rules
class PaymentProcessor {
processPayment(amount: number, method: string) {
if (method === "creditcard") {
// Credit card logic
} else if (method === "paypal") {
// PayPal logic
} else if (method === "crypto") {
// Crypto logic
}
// Adding new method requires modifying this function!
}
}class PaymentProcessor {
process(amount: number, strategy: PaymentStrategy) {
strategy.pay(amount);
}
}
// Adding new methods doesn't require changing existing code!Strategy pattern makes testing easier:
- Test each strategy in isolation
- Mock strategies when testing context
- Test strategy switching logic separately
- State Pattern: Similar structure, different intent
- Template Method: Defines algorithm skeleton in base class
- Command Pattern: Encapsulates requests, not algorithms
- Factory Pattern: Often used to create strategies
The Strategy Pattern is essential for writing maintainable, extensible code. It's one of the most practical patterns and appears frequently in interviews. Master it by:
- Understanding when conditional logic should become strategies
- Practicing with real-world examples
- Explaining trade-offs clearly
- Being able to implement it quickly
Next: Try the Factory Pattern →