Marcelo Pastorino, Software Developer

Strategy Pattern

A behavioral design pattern that encapsulates interchangeable algorithms behind a common interface, selected at runtime.

The strategy pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one behind a shared interface, and makes them interchangeable at runtime. The client holds a reference to the abstraction and delegates the varying behavior to whichever concrete strategy it was given, without knowing which one it is.

The problem it solves

When a single operation has several variants, such as different ways to price an order, sort a list, compress a file, or authenticate a user, the naive approach is a growing chain of if/else or switch branches. That code is hard to extend, mixes unrelated logic in one place, and forces you to edit a working method every time a new case appears.

The strategy pattern replaces those branches with polymorphism. Each variant becomes its own class, and the choice of variant becomes a plug-in object rather than a conditional.

Structure

  • Strategy, the interface common to all supported algorithms.
  • Concrete strategies, one class per algorithm, each implementing the interface.
  • Context, the class that uses a strategy; it exposes the operation and delegates to the strategy it holds.
  • Client, selects a concrete strategy and injects it into the context.
Client injects an interchangeable strategy into the context

In code

A checkout context computes a total using an injected discount strategy. New pricing rules arrive as new classes; the context never changes.

// Concrete strategies: one class per algorithm
class NoDiscount {
  apply(total) {
    return total;
  }
}

class PercentageDiscount {
  constructor(percent) {
    this.percent = percent;
  }
  apply(total) {
    return total * (1 - this.percent / 100);
  }
}

// Context: delegates the varying behavior to whatever strategy it holds
class Checkout {
  constructor(discount) {
    this.discount = discount;
  }
  total(amount) {
    return this.discount.apply(amount);
  }
}

// Client selects the strategy at runtime
const checkout = new Checkout(new PercentageDiscount(10));
console.log(checkout.total(200)); // 180
// Strategy: the shared interface
interface DiscountStrategy {
  apply(total: number): number;
}

// Concrete strategies: one class per algorithm
class NoDiscount implements DiscountStrategy {
  apply(total: number): number {
    return total;
  }
}

class PercentageDiscount implements DiscountStrategy {
  constructor(private readonly percent: number) {}
  apply(total: number): number {
    return total * (1 - this.percent / 100);
  }
}

// Context: delegates the varying behavior to whatever strategy it holds
class Checkout {
  constructor(private readonly discount: DiscountStrategy) {}
  total(amount: number): number {
    return this.discount.apply(amount);
  }
}

// Client selects the strategy at runtime
const checkout = new Checkout(new PercentageDiscount(10));
console.log(checkout.total(200)); // 180
// Strategy: the shared interface
public interface IDiscountStrategy
{
    decimal Apply(decimal total);
}

// Concrete strategies: one class per algorithm
public class NoDiscount : IDiscountStrategy
{
    public decimal Apply(decimal total) => total;
}

public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percent;
    public PercentageDiscount(decimal percent) => _percent = percent;
    public decimal Apply(decimal total) => total * (1 - _percent / 100);
}

// Context: delegates the varying behavior to whatever strategy it holds
public class Checkout
{
    private readonly IDiscountStrategy _discount;
    public Checkout(IDiscountStrategy discount) => _discount = discount;
    public decimal Total(decimal amount) => _discount.Apply(amount);
}

// Client selects the strategy at runtime
var checkout = new Checkout(new PercentageDiscount(10));
Console.WriteLine(checkout.Total(200)); // 180
from typing import Protocol

# Strategy: the shared interface
class DiscountStrategy(Protocol):
    def apply(self, total: float) -> float: ...

# Concrete strategies: one class per algorithm
class NoDiscount:
    def apply(self, total: float) -> float:
        return total

class PercentageDiscount:
    def __init__(self, percent: float) -> None:
        self._percent = percent
    def apply(self, total: float) -> float:
        return total * (1 - self._percent / 100)

# Context: delegates the varying behavior to whatever strategy it holds
class Checkout:
    def __init__(self, discount: DiscountStrategy) -> None:
        self._discount = discount
    def total(self, amount: float) -> float:
        return self._discount.apply(amount)

# Client selects the strategy at runtime
checkout = Checkout(PercentageDiscount(10))
print(checkout.total(200))  # 180

Relationship to SOLID

Strategy is a direct application of two SOLID principles. It satisfies the open/closed principle because new algorithms arrive as new classes, so the context is open for extension but closed for modification. It also relies on the dependency inversion principle. The context depends on the strategy abstraction, not on any concrete algorithm.

Strategy vs alternatives

  • State pattern, shares the same structure, but state transitions are driven by the objects themselves as the context moves through a lifecycle, whereas a strategy is chosen once by the client and rarely changes itself.
  • Template method pattern, varies steps of an algorithm through inheritance and overriding; strategy varies the whole algorithm through composition, which is more flexible and swappable at runtime.
  • Simple functions, in languages with first-class functions, a strategy is often just a function passed as an argument. Reach for classes only when a strategy needs its own state or several related methods.

Trade-offs

  • Adds classes and indirection; for two rarely changing branches, a plain conditional is clearer.
  • The client must know enough to pick the right strategy, which pushes the decision outward (often into configuration or a factory).
  • Best when variants share a stable interface and the set of algorithms genuinely changes or grows over time.

See also

  • Template Method Pattern, A behavioral design pattern that defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall flow.
  • State Pattern, A behavioral design pattern that lets an object change its behavior when its internal state changes, by delegating to interchangeable state objects.
  • Observer Pattern, A behavioral design pattern that lets dependents subscribe to a subject and receive automatic updates when its state changes, without tight coupling between them.
  • Adapter Pattern, A structural design pattern that wraps an incompatible interface in one the client expects, letting otherwise unrelated types work together.
  • SOLID Principles, Five object-oriented design principles for maintainable, change-tolerant code.

On Wikipedia