designpattern.site

Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable without changing the code that uses them.

Overview

Real-world problems rarely have a single best solution. Sorting a nearly-sorted list calls for a different algorithm than sorting a million random integers. Charging a customer with a credit card follows completely different rules than charging with PayPal or a bank transfer. Writing that logic as a chain of if/else blocks or switch statements works at first, but every new algorithm requires touching code that should not need to change.

The Strategy pattern is a behavioral design pattern that solves this by pulling each algorithm into its own class behind a shared interface. The object that needs the algorithm holds a reference to a strategy, not to the concrete implementation. Swapping strategies at runtime becomes trivial, and adding a new one requires no changes to existing code.

The pattern is a direct application of the Open/Closed Principle: open for extension (add a new strategy class), closed for modification (the context class never changes).

Problem & Motivation

Imagine you are building a payment processing system. At launch you support credit cards. Six months later you add PayPal. A year after that you add cryptocurrency. Each payment method has different validation rules, fee calculations, and API calls.

The tempting approach is to add a payment type field and branch on it:

if type == "credit_card": ...
elif type == "paypal": ...
elif type == "crypto": ...

Every new payment method requires modifying this block. Every modification risks breaking existing methods. The block grows longer over time and becomes harder to test in isolation. The class that orchestrates checkout now knows implementation details of every payment provider — a clear violation of separation of concerns.

Strategy fixes this by expressing the variation as a first-class object rather than a branch in code.

Class Diagram

Loading diagram...

The structure has three roles:

  • Strategy interface (PaymentStrategy) — declares the method signature all concrete strategies must implement.
  • Concrete strategies (CreditCardStrategy, PayPalStrategy, CryptoStrategy) — each encapsulates one algorithm.
  • Context (PaymentContext) — holds a reference to a strategy and delegates work to it. The context does not know or care which concrete strategy it holds.

Implementation

The implementations below model a checkout flow where the payment method can be swapped at runtime. All four versions produce the same observable behavior: the context delegates to whichever strategy is currently set, and you can switch strategies without touching the context class.

interface PaymentStrategy {
  pay(amount: number): string;
}

class CreditCardStrategy implements PaymentStrategy {
  constructor(private cardNumber: string) {}

  pay(amount: number): string {
    return `Charged $${amount} to credit card ending in ${this.cardNumber.slice(-4)}`;
  }
}

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

  pay(amount: number): string {
    return `Sent $${amount} via PayPal to ${this.email}`;
  }
}

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

  pay(amount: number): string {
    return `Transferred $${amount} worth of crypto to ${this.walletAddress}`;
  }
}

class PaymentContext {
  private strategy: PaymentStrategy;

  constructor(strategy: PaymentStrategy) {
    this.strategy = strategy;
  }

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

  checkout(amount: number): string {
    return this.strategy.pay(amount);
  }
}

// Usage
const context = new PaymentContext(new CreditCardStrategy("4111111111111234"));
console.log(context.checkout(99.99));
// Charged $99.99 to credit card ending in 1234

context.setStrategy(new PayPalStrategy("user@example.com"));
console.log(context.checkout(49.00));
// Sent $49 via PayPal to user@example.com

context.setStrategy(new CryptoStrategy("0xABC123"));
console.log(context.checkout(200.00));
// Transferred $200 worth of crypto to 0xABC123

Strategy vs. Simple Functions

In languages with first-class functions (TypeScript, Python, Go), you can pass a function directly instead of wrapping it in a class. This is idiomatic for simple cases: context.setStrategy((amount) => ...). Reserve strategy classes for cases where the algorithm needs configuration state (like a card number or email) or multiple related methods. If your strategy interface has one method and no state, a function or lambda is the leaner choice.

Real-World Examples

Sorting algorithmsArray.prototype.sort() in JavaScript and Arrays.sort() in Java both accept a comparator function. That comparator is a strategy: it defines the ordering rule without changing the sort implementation. You can sort by price, by date, or by relevance simply by swapping the comparator.

Compression libraries — Tools like zlib or archive utilities offer multiple compression formats (gzip, bzip2, lzma). The caller picks a strategy based on the trade-off between speed and compression ratio. The code that reads and writes bytes does not change — only the compression strategy does.

Authentication middleware — Frameworks like Passport.js (Node) or Spring Security build their authentication pipeline around the Strategy pattern. Each auth method (JWT, OAuth2, local password) is a discrete strategy. The middleware context delegates to whichever strategy is registered for the current route, making it straightforward to add or remove auth methods without touching the core framework code.

Discount and pricing engines — E-commerce platforms apply different pricing rules depending on user tier, coupon codes, or promotional periods. Each rule (percentage off, flat discount, buy-one-get-one) is a strategy. The checkout service holds a list of active strategies and applies them in sequence.

Pros and Cons

Advantages

Open/Closed compliance — adding a new algorithm means adding a new class, not editing existing code. Existing strategies and the context remain untouched.

Testability — each strategy is an isolated class with a single method. Unit tests for CreditCardStrategy have no knowledge of PayPalStrategy and vice versa.

Eliminates conditionals — replaces branching if/else or switch blocks with polymorphism, which scales better as the number of variants grows.

Runtime flexibility — strategies can be swapped while the application is running, enabling dynamic behavior changes based on user preferences, feature flags, or system state.

Separation of concerns — the context focuses on orchestration; each strategy focuses purely on its algorithm. Neither knows the implementation details of the other.

Disadvantages

Class proliferation — every algorithm becomes a class. For a system with many simple variations, this can produce a large number of small files that add navigational overhead.

Clients must be aware of strategies — the code that creates the context must know which strategy to inject. This knowledge has to live somewhere; Strategy does not eliminate that decision, it relocates it.

Overhead for simple cases — if you only have two algorithms and they will never change, an if/else is simpler and just as correct. Strategy adds indirection that only pays off when the number of variants grows or changes frequently.

Interface rigidity — all strategies share the same interface. If two algorithms need fundamentally different inputs, forcing them through one method signature results in awkward parameter passing or strategy-specific setup before the call.

When to Use / When to Avoid

Use Strategy when:

  • You have multiple variants of an algorithm and want to switch between them at runtime.
  • You want to eliminate conditional branches that select between behaviors.
  • A class has behavior that its subclasses override, and you want to extract that variation without deep inheritance hierarchies.
  • Algorithms need to be independently tested and maintained by different team members.
  • You anticipate that new variants will be added over time and want to minimize the blast radius of each addition.

Avoid Strategy when:

  • You only have two algorithms and the choice is fixed at compile time — a simple if/else is more readable.
  • The strategies need to share a significant amount of private state from the context — tight coupling between context and strategy often signals the behavior belongs inside the context itself.
  • The algorithm selection is so simple (a boolean flag, a single enum) that the indirection of a strategy object adds more complexity than it removes.
  • Performance is extremely critical and the cost of virtual dispatch or extra object allocation is measurable in your profiling data.

State — structurally identical to Strategy (both use a context that delegates to a swappable object), but the intent differs. Strategy swaps algorithms based on external input; State swaps behavior based on the object's own internal state, and the state object itself can trigger transitions. If the algorithm being swapped decides when to swap, you likely want State instead.

Template Method — achieves algorithm variation through inheritance rather than composition. The base class defines the skeleton of the algorithm, and subclasses fill in specific steps. Strategy and Template Method solve the same problem; prefer Strategy when you want to avoid inheritance and when you need runtime swapping. Template Method is simpler when the variation is known at compile time and the steps share significant shared logic.

Command — encapsulates a request as an object, similar to how Strategy encapsulates an algorithm. The difference is intent: Command focuses on queueing, logging, or undoing operations; Strategy focuses on selecting how an operation is performed. A payment processor that logs and retries failed commands might use Command to wrap individual payment calls and Strategy to decide which payment provider to use.

Decorator — adds behavior to an object dynamically by wrapping it. Strategy replaces behavior; Decorator extends it. A pricing engine might use Strategy to select the base pricing algorithm and Decorator to layer on sequential discounts.

Design Tip

When you find yourself writing a method that starts with if paymentType == ... elif paymentType == ... — stop. That is a signal that the varying behavior is a candidate for extraction into a strategy. Map each branch to a concrete strategy class, define an interface around the shared signature, and inject the right strategy at the call site. The context method collapses to a single line, and each branch becomes independently testable.

On this page