← Back to principles

Design Principle

Strategy Pattern

Learn the Strategy pattern: define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients.

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.


What is the Strategy Pattern?

Strategy Pattern provides:

  • Algorithm encapsulation: Encapsulate algorithms in separate classes
  • Interchangeability: Algorithms are interchangeable
  • Runtime selection: Select algorithm at runtime
  • Open/Closed Principle: Open for extension, closed for modification

Use cases:

  • Sorting algorithms
  • Payment processing
  • Compression algorithms
  • Validation strategies
  • Discount calculations

Structure

Context
  └─ strategy: Strategy
  └─ execute() (uses strategy.algorithm())

Strategy (interface)
  └─ algorithm()

ConcreteStrategyA (implements Strategy)
  └─ algorithm()

ConcreteStrategyB (implements Strategy)
  └─ algorithm()

Examples

Payment Strategy

// Strategy interface
interface PaymentStrategy {
  pay(amount: number): void;
}

// Concrete strategies
class CreditCardStrategy implements PaymentStrategy {
  constructor(private cardNumber: string, private cvv: string) {}
  
  pay(amount: number): void {
    console.log(`Paid $${amount} using credit card ending in ${this.cardNumber.slice(-4)}`);
  }
}

class PayPalStrategy implements PaymentStrategy {
  constructor(private email: string) {}
  
  pay(amount: number): void {
    console.log(`Paid $${amount} using PayPal account ${this.email}`);
  }
}

class BankTransferStrategy implements PaymentStrategy {
  constructor(private accountNumber: string) {}
  
  pay(amount: number): void {
    console.log(`Paid $${amount} using bank transfer to account ${this.accountNumber}`);
  }
}

// Context
class PaymentProcessor {
  private strategy: PaymentStrategy;
  
  constructor(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy;
  }
  
  processPayment(amount: number): void {
    this.strategy.pay(amount);
  }
}

// Usage
const processor = new PaymentProcessor(new CreditCardStrategy("1234567890", "123"));
processor.processPayment(100);

processor.setStrategy(new PayPalStrategy("user@example.com"));
processor.processPayment(200);

Sorting Strategy

// Strategy interface
interface SortStrategy {
  sort(data: number[]): number[];
}

// Concrete strategies
class BubbleSortStrategy implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Sorting using bubble sort");
    const arr = [...data];
    for (let i = 0; i < arr.length; i++) {
      for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSortStrategy implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Sorting using quick sort");
    return this.quickSort([...data]);
  }
  
  private quickSort(arr: number[]): number[] {
    if (arr.length <= 1) return arr;
    
    const pivot = arr[Math.floor(arr.length / 2)];
    const left = arr.filter(x => x < pivot);
    const middle = arr.filter(x => x === pivot);
    const right = arr.filter(x => x > pivot);
    
    return [...this.quickSort(left), ...middle, ...this.quickSort(right)];
  }
}

class MergeSortStrategy implements SortStrategy {
  sort(data: number[]): number[] {
    console.log("Sorting using merge sort");
    return this.mergeSort([...data]);
  }
  
  private mergeSort(arr: number[]): number[] {
    if (arr.length <= 1) return arr;
    
    const mid = Math.floor(arr.length / 2);
    const left = this.mergeSort(arr.slice(0, mid));
    const right = this.mergeSort(arr.slice(mid));
    
    return this.merge(left, right);
  }
  
  private merge(left: number[], right: number[]): number[] {
    const result: number[] = [];
    let i = 0, j = 0;
    
    while (i < left.length && j < right.length) {
      if (left[i] <= right[j]) {
        result.push(left[i++]);
      } else {
        result.push(right[j++]);
      }
    }
    
    return result.concat(left.slice(i)).concat(right.slice(j));
  }
}

// Context
class Sorter {
  private strategy: SortStrategy;
  
  constructor(strategy: SortStrategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: SortStrategy): void {
    this.strategy = strategy;
  }
  
  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

// Usage
const data = [64, 34, 25, 12, 22, 11, 90];

const sorter = new Sorter(new BubbleSortStrategy());
console.log(sorter.sort(data));

sorter.setStrategy(new QuickSortStrategy());
console.log(sorter.sort(data));

sorter.setStrategy(new MergeSortStrategy());
console.log(sorter.sort(data));

Discount Strategy

// Strategy interface
interface DiscountStrategy {
  calculateDiscount(price: number): number;
}

// Concrete strategies
class NoDiscountStrategy implements DiscountStrategy {
  calculateDiscount(price: number): number {
    return 0;
  }
}

class PercentageDiscountStrategy implements DiscountStrategy {
  constructor(private percentage: number) {}
  
  calculateDiscount(price: number): number {
    return price * (this.percentage / 100);
  }
}

class FixedDiscountStrategy implements DiscountStrategy {
  constructor(private amount: number) {}
  
  calculateDiscount(price: number): number {
    return Math.min(this.amount, price);
  }
}

class SeasonalDiscountStrategy implements DiscountStrategy {
  calculateDiscount(price: number): number {
    const month = new Date().getMonth();
    // 20% discount in December
    if (month === 11) {
      return price * 0.2;
    }
    return 0;
  }
}

// Context
class PriceCalculator {
  private discountStrategy: DiscountStrategy;
  
  constructor(discountStrategy: DiscountStrategy) {
    this.discountStrategy = discountStrategy;
  }
  
  setDiscountStrategy(strategy: DiscountStrategy): void {
    this.discountStrategy = strategy;
  }
  
  calculateFinalPrice(price: number): number {
    const discount = this.discountStrategy.calculateDiscount(price);
    return price - discount;
  }
}

// Usage
const calculator = new PriceCalculator(new NoDiscountStrategy());
console.log(calculator.calculateFinalPrice(100)); // 100

calculator.setDiscountStrategy(new PercentageDiscountStrategy(10));
console.log(calculator.calculateFinalPrice(100)); // 90

calculator.setDiscountStrategy(new FixedDiscountStrategy(20));
console.log(calculator.calculateFinalPrice(100)); // 80

calculator.setDiscountStrategy(new SeasonalDiscountStrategy());
console.log(calculator.calculateFinalPrice(100)); // 80 (if December)

Common Pitfalls

  • Too many strategies: Can become complex. Fix: Group related strategies
  • Strategy selection: Hard to choose strategy. Fix: Use factory pattern
  • Context bloat: Context becomes too large. Fix: Keep context simple
  • Not using strategy: Using if-else instead. Fix: Use strategy pattern for multiple algorithms

Interview Questions

Beginner

Q: What is the Strategy pattern and when would you use it?

A:

Strategy Pattern defines a family of algorithms, encapsulates each, and makes them interchangeable.

Key characteristics:

  • Algorithm encapsulation: Each algorithm in separate class
  • Interchangeability: Algorithms are interchangeable
  • Runtime selection: Select algorithm at runtime
  • Open/Closed: Open for extension, closed for modification

Example:

interface SortStrategy {
  sort(data: number[]): number[];
}

class QuickSort implements SortStrategy {
  sort(data: number[]): number[] { /* ... */ }
}

class MergeSort implements SortStrategy {
  sort(data: number[]): number[] { /* ... */ }
}

class Sorter {
  private strategy: SortStrategy;
  sort(data: number[]): number[] {
    return this.strategy.sort(data);
  }
}

Use cases:

  • Multiple algorithms: Different ways to do same thing
  • Runtime selection: Choose algorithm at runtime
  • Payment methods: Credit card, PayPal, etc.
  • Sorting algorithms: Quick sort, merge sort, etc.

Benefits:

  • Flexibility: Easy to add new algorithms
  • Testability: Test each strategy independently
  • Maintainability: Changes isolated to strategy classes

Intermediate

Q: Explain how the Strategy pattern works. How does it differ from if-else or switch statements?

A:

Strategy Pattern Structure:

1. Strategy Interface:

interface Strategy {
  algorithm(): void;
}

2. Concrete Strategies:

class StrategyA implements Strategy {
  algorithm(): void {
    // Algorithm A
  }
}

class StrategyB implements Strategy {
  algorithm(): void {
    // Algorithm B
  }
}

3. Context:

class Context {
  private strategy: Strategy;
  
  setStrategy(strategy: Strategy): void {
    this.strategy = strategy;
  }
  
  execute(): void {
    this.strategy.algorithm();
  }
}

Difference from If-Else:

If-Else (Not Strategy):

function process(type: string) {
  if (type === "A") {
    // Algorithm A
  } else if (type === "B") {
    // Algorithm B
  }
}

Problems:

  • Not extensible: Need to modify function for new types
  • Violates Open/Closed: Closed for extension
  • Hard to test: Can't test algorithms independently

Strategy Pattern:

class Context {
  private strategy: Strategy;
  
  execute(): void {
    this.strategy.algorithm(); // Extensible, testable
  }
}

Benefits:

  • Extensible: Add new strategies without modifying context
  • Testable: Test each strategy independently
  • Maintainable: Changes isolated to strategy classes

Senior

Q: Design a strategy system for a recommendation engine that uses different recommendation algorithms (collaborative filtering, content-based, hybrid). Handle algorithm selection based on user preferences, data availability, and performance requirements.

A:

// Strategy interface
interface RecommendationStrategy {
  recommend(userId: string, limit: number): Promise<Recommendation[]>;
  getAlgorithmName(): string;
  getPerformanceMetrics(): PerformanceMetrics;
}

// Collaborative filtering strategy
class CollaborativeFilteringStrategy implements RecommendationStrategy {
  async recommend(userId: string, limit: number): Promise<Recommendation[]> {
    // Find similar users
    const similarUsers = await this.findSimilarUsers(userId);
    
    // Get items liked by similar users
    const recommendations = await this.getItemsFromSimilarUsers(similarUsers);
    
    // Rank and return top N
    return this.rankRecommendations(recommendations).slice(0, limit);
  }
  
  getAlgorithmName(): string {
    return "Collaborative Filtering";
  }
  
  getPerformanceMetrics(): PerformanceMetrics {
    return {
      accuracy: 0.85,
      latency: 200, // ms
      requiresUserData: true
    };
  }
  
  private async findSimilarUsers(userId: string): Promise<string[]> {
    // Find users with similar preferences
    return [];
  }
  
  private async getItemsFromSimilarUsers(users: string[]): Promise<Recommendation[]> {
    // Get items from similar users
    return [];
  }
  
  private rankRecommendations(items: Recommendation[]): Recommendation[] {
    // Rank by relevance
    return items.sort((a, b) => b.score - a.score);
  }
}

// Content-based strategy
class ContentBasedStrategy implements RecommendationStrategy {
  async recommend(userId: string, limit: number): Promise<Recommendation[]> {
    // Get user preferences
    const preferences = await this.getUserPreferences(userId);
    
    // Find items with similar features
    const recommendations = await this.findSimilarItems(preferences);
    
    return recommendations.slice(0, limit);
  }
  
  getAlgorithmName(): string {
    return "Content-Based";
  }
  
  getPerformanceMetrics(): PerformanceMetrics {
    return {
      accuracy: 0.75,
      latency: 150,
      requiresUserData: false
    };
  }
  
  private async getUserPreferences(userId: string): Promise<Preferences> {
    // Get user's past preferences
    return {};
  }
  
  private async findSimilarItems(preferences: Preferences): Promise<Recommendation[]> {
    // Find items matching preferences
    return [];
  }
}

// Hybrid strategy
class HybridStrategy implements RecommendationStrategy {
  private strategies: RecommendationStrategy[];
  
  constructor() {
    this.strategies = [
      new CollaborativeFilteringStrategy(),
      new ContentBasedStrategy()
    ];
  }
  
  async recommend(userId: string, limit: number): Promise<Recommendation[]> {
    // Get recommendations from all strategies
    const allRecommendations = await Promise.all(
      this.strategies.map(s => s.recommend(userId, limit * 2))
    );
    
    // Merge and deduplicate
    const merged = this.mergeRecommendations(allRecommendations);
    
    // Re-rank
    return this.rerank(merged).slice(0, limit);
  }
  
  getAlgorithmName(): string {
    return "Hybrid";
  }
  
  getPerformanceMetrics(): PerformanceMetrics {
    return {
      accuracy: 0.90,
      latency: 300,
      requiresUserData: true
    };
  }
  
  private mergeRecommendations(recommendations: Recommendation[][]): Recommendation[] {
    // Merge and deduplicate
    const map = new Map<string, Recommendation>();
    
    recommendations.forEach(list => {
      list.forEach(rec => {
        const existing = map.get(rec.itemId);
        if (!existing || existing.score < rec.score) {
          map.set(rec.itemId, rec);
        }
      });
    });
    
    return Array.from(map.values());
  }
  
  private rerank(recommendations: Recommendation[]): Recommendation[] {
    // Re-rank merged recommendations
    return recommendations.sort((a, b) => b.score - a.score);
  }
}

// Strategy selector
class RecommendationStrategySelector {
  selectStrategy(context: RecommendationContext): RecommendationStrategy {
    // Select based on context
    if (context.hasUserData && context.performanceRequirement === "high") {
      return new CollaborativeFilteringStrategy();
    } else if (!context.hasUserData) {
      return new ContentBasedStrategy();
    } else {
      return new HybridStrategy();
    }
  }
}

// Context
class RecommendationEngine {
  private strategy: RecommendationStrategy;
  
  constructor(strategy: RecommendationStrategy) {
    this.strategy = strategy;
  }
  
  setStrategy(strategy: RecommendationStrategy): void {
    this.strategy = strategy;
  }
  
  async getRecommendations(userId: string, limit: number): Promise<Recommendation[]> {
    return await this.strategy.recommend(userId, limit);
  }
  
  getPerformanceMetrics(): PerformanceMetrics {
    return this.strategy.getPerformanceMetrics();
  }
}

// Usage
const selector = new RecommendationStrategySelector();
const context = {
  hasUserData: true,
  performanceRequirement: "high"
};

const strategy = selector.selectStrategy(context);
const engine = new RecommendationEngine(strategy);
const recommendations = await engine.getRecommendations("user123", 10);

Features:

  1. Multiple strategies: Different recommendation algorithms
  2. Strategy selection: Select based on context
  3. Performance metrics: Track algorithm performance
  4. Hybrid approach: Combine multiple strategies

Key Takeaways

  • Strategy pattern: Encapsulates algorithms, makes them interchangeable
  • Runtime selection: Select algorithm at runtime
  • Open/Closed Principle: Open for extension, closed for modification
  • Use cases: Multiple algorithms, payment methods, sorting, validation
  • Benefits: Flexibility, testability, maintainability
  • Best practices: Use factory for selection, keep context simple, group related strategies

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.