Detail chart · Backend

Strategy Pattern

Pattern ◆◆◇◇◇

Define a family of algorithms behind a common interface and make them interchangeable at runtime.

Summary

The Strategy Pattern defines a set of interchangeable behaviors (strategies) behind a shared interface. The consuming class delegates to whichever strategy is injected, enabling behavior to vary without modifying the class itself. It is the behavioral realization of the Open/Closed Principle.

Problem

Conditional logic that selects between multiple algorithms or behaviors tends to grow. Each new case requires modifying existing code, increasing risk and complexity.

Solution

Extract each algorithm or behavior variant into its own class that implements a shared interface. The context class holds a reference to the interface and delegates to it.

interface ShippingStrategy {
  calculateCost(order: Order): number;
}

class StandardShipping implements ShippingStrategy {
  calculateCost(order: Order) { return order.weight * 2.5; }
}

class ExpressShipping implements ShippingStrategy {
  calculateCost(order: Order) { return order.weight * 5.0 + 10; }
}

class OrderService {
  constructor(private shipping: ShippingStrategy) {}

  getTotal(order: Order) {
    return order.subtotal + this.shipping.calculateCost(order);
  }
}

Strategies are swapped via constructor injection or a setter. The context code never changes when new strategies are added.

The same shape holds in Python, using Protocol for the interface and constructor injection for the swap:

from typing import Protocol

class ShippingStrategy(Protocol):
    def calculate_cost(self, order: "Order") -> float: ...

class StandardShipping:
    def calculate_cost(self, order: "Order") -> float:
        return order.weight * 2.5

class ExpressShipping:
    def calculate_cost(self, order: "Order") -> float:
        return order.weight * 5.0 + 10

class OrderService:
    def __init__(self, shipping: ShippingStrategy):
        self._shipping = shipping

    def get_total(self, order: "Order") -> float:
        return order.subtotal + self._shipping.calculate_cost(order)

# Swap strategies without touching OrderService
standard_total = OrderService(StandardShipping()).get_total(order)
express_total = OrderService(ExpressShipping()).get_total(order)

Protocol gives structural typing — any class with a matching calculate_cost method satisfies ShippingStrategy, with no explicit inheritance required.

Live Playground

Experiment with the pattern below. The two tabs show a checkout flow before extraction, where shipping cost logic lives in an if/else chain inside OrderService, and the after version where each shipping method is its own ShippingStrategy implementation swapped in at runtime.

When to Use

Avoid when:

Trade-offs

BenefitCost
New behaviors added without modifying existing codeMore classes/files to manage
Each strategy is independently testableCallers must be aware of available strategies
Eliminates conditional logic from context classCan feel like over-engineering for simple cases
Strategies are composable (wrap one in another)Interface must be stable — changing it touches all implementations