Skip to content

Strategy Pattern β€” Interchangeable Algorithms (2026)

DodaTech Updated 2026-06-20 5 min read

In this tutorial, you'll learn how the strategy pattern lets you swap algorithms at runtime, how it eliminates massive if-else chains, and how real payment systems use it to support multiple processors.

Think of a GPS navigation app. You need directions from A to B. The app offers multiple strategies: "shortest distance," "fastest route," "avoid tolls," "scenic route." Each strategy calculates the route differently, but they all return the same thing β€” directions. You can switch strategies at any time without changing the navigation system. The strategy pattern is exactly this: a family of algorithms, each encapsulated and interchangeable.

Core Concept

The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The algorithm can vary independently from the clients that use it. Instead of a single class with multiple conditional branches, each algorithm gets its own class implementing a common interface.

// The strategy interface
interface PaymentStrategy {
  pay(amount: number): Promise<PaymentResult>;
}

// Concrete strategies
class CreditCardStrategy implements PaymentStrategy {
  constructor(private cardNumber: string, private cvv: string) {}

  async pay(amount: number): Promise<PaymentResult> {
    // Process credit card payment
    const response = await creditCardApi.charge(this.cardNumber, amount);
    return { success: true, transactionId: response.id };
  }
}

class PayPalStrategy implements PaymentStrategy {
  constructor(private email: string) {}

  async pay(amount: number): Promise<PaymentResult> {
    const response = await paypalApi.transfer(this.email, amount);
    return { success: true, transactionId: response.id };
  }
}

class CryptoStrategy implements PaymentStrategy {
  constructor(private walletAddress: string) {}

  async pay(amount: number): Promise<PaymentResult> {
    const tx = await cryptoApi.send(this.walletAddress, amount);
    return { success: true, transactionId: tx.hash };
  }
}

// The context class uses any strategy
class CheckoutService {
  constructor(private strategy: PaymentStrategy) {}

  setStrategy(strategy: PaymentStrategy): void {
    this.strategy = strategy;
  }

  async checkout(amount: number): Promise<PaymentResult> {
    // Before payment logic
    this.logger.info(`Processing payment of $${amount}`);
    // Delegate to the strategy
    const result = await this.strategy.pay(amount);
    // After payment logic
    this.logger.info(`Payment ${result.success ? "succeeded" : "failed"}`);
    return result;
  }
}

How It Works

The pattern has three participants:

  • Context β€” the class that uses a strategy (e.g., CheckoutService)
  • Strategy interface β€” the contract all strategies implement
  • Concrete strategies β€” the actual algorithms
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Context        β”‚ uses β†’ β”‚ Β«Strategy InterfaceΒ» β”‚
β”‚  (CheckoutService)β”‚        β”‚  + pay(amount)       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    ↑
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚         β”‚         β”‚
               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
               β”‚ CreditCard   β”‚ β”‚  PayPal  β”‚ β”‚  Crypto  β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The client creates a strategy and passes it to the context. The context calls the strategy's method without knowing which concrete implementation is running. Strategies can be swapped at runtime via a setter.

Eliminating Switch Statements

// ❌ Without Strategy β€” growing if-else chain
function processPayment(type: string, amount: number) {
  if (type === "credit_card") {
    // 50 lines of credit card logic
  } else if (type === "paypal") {
    // 50 lines of PayPal logic
  } else if (type === "crypto") {
    // 50 lines of crypto logic
  }
  // Adding a new type = modifying this function
}

// βœ… With Strategy β€” each algorithm is isolated
const strategies: Record<string, PaymentStrategy> = {
  credit_card: new CreditCardStrategy(cardNum, cvv),
  paypal: new PayPalStrategy(email),
  crypto: new CryptoStrategy(wallet),
};

const strategy = strategies[type];
const result = await strategy.pay(amount);
// Adding a new type = adding a new class & registering it

Expected output: The strategy version is open for extension (add a new class) but closed for modification (no changes to existing code). This follows the Open/Closed Principle.

Real-World Examples

File Compression

DodaZIP uses the strategy pattern for compression algorithms. The context is the Compressor class, and strategies include ZipStrategy, GzipStrategy, TarStrategy, and SevenZipStrategy. Users select the format at runtime, and the compressor delegates to the appropriate algorithm.

Validation Rules

Form validation often uses the strategy pattern. Each validation rule (required, email format, min length, unique check) is a strategy. The form validator receives a list of strategies and runs them in sequence. Adding a new validation doesn't require changing the validator class.

interface ValidationStrategy {
  validate(value: unknown): string | null; // null = valid, string = error
}

class EmailValidator implements ValidationStrategy {
  validate(value: unknown): string | null {
    const email = value as string;
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
      ? null : "Invalid email format";
  }
}

class FormValidator {
  constructor(private strategies: ValidationStrategy[]) {}

  validate(data: Record<string, unknown>): Record<string, string> {
    const errors: Record<string, string> = {};
    for (const [field, strategies] of Object.entries(this.strategies)) {
      // Each field has its own set of strategies
    }
    return errors;
  }
}

Pros & Cons

Pros Cons
Eliminates massive conditional statements Increases number of classes
Follows Open/Closed Principle Clients must know about available strategies
Algorithms can be reused across contexts Over-engineering if only 1-2 algorithms exist
Strategies can be swapped at runtime Strategy selection logic can grow complex

When to Use

Use the strategy pattern when:

  • You have multiple related algorithms that differ only in behavior
  • You need to swap algorithms at runtime
  • You want to avoid conditional statements for algorithm selection
  • The algorithm tends to change or grow β€” isolating it prevents cascading changes

Avoid it when you have only one algorithm that's unlikely to change, or when the conditional logic is simple and unlikely to grow.

FAQ

How is this different from polymorphism?

Strategy is a specific use of polymorphism. Regular polymorphism lets subclasses override methods. Strategy makes those overrides into standalone, interchangeable classes that can be swapped at runtime via composition rather than inheritance.

Can strategies have different method signatures?

No β€” all strategies must implement the same interface. If strategies need different data, pass it through the constructor (for strategy-specific config) or use a parameter object that contains everything strategies might need.

How does this compare to the Factory Pattern?

The Factory Pattern creates objects. The strategy pattern encapsulates behavior. They're complementary β€” a factory often decides which strategy to create based on configuration or input. Factories handle object creation; strategies handle algorithm selection.

Should strategies be stateless?

Preferably yes β€” pass all context through method parameters. Stateless strategies are easier to test and can be shared across contexts. If a strategy needs state, manage it carefully and ensure thread safety.

How do I handle default strategies?

Provide a reasonable default in the context class. If no strategy is explicitly set, use the default. For example, a DefaultSortStrategy that returns items in insertion order, or a DefaultPaymentStrategy that uses the most common payment method

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro