Skip to contentJMRG
All posts

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 <|.. ConcreteStrategyB

In 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).