Skip to content
aipatterns.dev

Design patternBehavioral Patterns

Strategy

Put each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.

Complexity
Low
Seen in the wild
High
Also known as
Policy

Intent

Define a family of algorithms that do the same job in different ways, give them one shared interface, and let the calling code hold a reference to whichever one it needs. The caller stops knowing how the work is done and only knows that it can be done.

Forces

  • One stable caller vs many ways to do its core step: the surrounding workflow rarely changes, but the step in the middle does.
  • Choosing at runtime vs readable code: a growing switch over modes works until every new mode touches the same function.
  • Testing each variant in isolation vs variants tangled in one class: you can't unit test the third branch of a 200-line method on its own.
  • Adding a variant vs editing code that already works: each new case should be new code, not a change to shipped code.

Therefore

Move each variant into its own object behind a common interface, and have the caller delegate to that object instead of branching.

The caller (the context) receives a strategy through its constructor or a setter. To add a behavior, you write a new strategy; the context does not change. In languages with first-class functions, a strategy is often just a function value. Reach for an interface only when a strategy needs state, configuration, or several related methods.

Structure

Diagram source (Mermaid)
classDiagram
  class Checkout {
    -ShippingStrategy shipping
    +setShipping(s) void
    +total(order) Money
  }
  class ShippingStrategy {
    <<interface>>
    +quote(order) Money
  }
  class FlatRate {
    +quote(order) Money
  }
  class ByWeight {
    +quote(order) Money
  }
  class FreeOverThreshold {
    +quote(order) Money
  }
  Checkout o-- ShippingStrategy
  ShippingStrategy <|.. FlatRate
  ShippingStrategy <|.. ByWeight
  ShippingStrategy <|.. FreeOverThreshold

Participants

  • Strategy (ShippingStrategy): the interface every variant implements. Keep it to what the context actually calls.
  • Concrete strategies (FlatRate, ByWeight, …): one algorithm each, with no knowledge of each other.
  • Context (Checkout): holds a strategy, calls it at the right moment, and never inspects its concrete type.
  • Client: picks a concrete strategy and hands it to the context, usually from configuration or user input.

Use it when

  • A function has a switch or if/else chain that selects between algorithms (not data), and new branches keep arriving.
  • Several classes differ only in one method's behavior.
  • You need to swap behavior at runtime, such as by tenant, feature flag, A/B bucket, or user setting.
  • You want to test each algorithm on its own with no setup for the others.
  • An algorithm uses data or dependencies the rest of the class shouldn't see.

Do not use it when

  • There are only two variants and no realistic third. A boolean parameter or a plain if is clearer.
  • The variant is a single stateless operation. Pass a function ((order) => Money); in a language with first-class functions, a one-method interface and three classes is ceremony. Keep the interface when variants carry configuration, mutable state, or a family of related methods.
  • The variants change what happens across many methods together, depending on the object's current situation. That is State.
  • The variation is a few steps inside a fixed skeleton. Template Method or plain helper functions fit better.
  • The caller would have to know every concrete strategy anyway to choose one, and the choice never changes after construction. A factory function returning the right implementation, or a lookup table, may be all you need.

Application grill

Inspect first: the branching on algorithm choice and every site that switches on it; whether the codebase already passes functions (idiomatic in TypeScript, Python, Go); whether the choice comes from code, configuration, or per-tenant data.

Missing pieces: none beyond the language: pass a function. A strategy registry keyed by name earns its place only when the choice arrives from configuration or user input.

Ask before you build:

  • Scale: how many real variants exist today - fewer than three with no third planned is a conditional?
  • Performance: is dispatch on a hot path where a registry lookup shows (a direct call is cheapest)?
  • Reliability: when configuration names an unknown strategy, does it fail at startup or at first use?
  • Budget: N files for N strategies - is the set actually changing, or frozen?
  • Implementation: what context does each strategy need - one shared interface, or per-strategy parameters?

Implementation steps

  1. Find the branching code and confirm each branch computes the same kind of result from the same inputs.
  2. Write the strategy interface from what the context passes in and needs back. Pass data in as arguments; avoid handing strategies the whole context.
  3. Move each branch body into its own strategy class or function. Keep behavior identical and run the tests after each move.
  4. Give the context a field for the strategy, set through its constructor (or a setter if runtime swapping is required).
  5. Replace the branching with one call to the strategy.
  6. Move the selection logic (which strategy to use) to the edge: configuration, a factory, or a map from key to strategy.

Example

An online store quotes shipping in three ways, and the marketing team adds a new rule every quarter.

type Money = number; // cents

interface Order {
  subtotal: Money;
  weightKg: number;
}

interface ShippingStrategy {
  quote(order: Order): Money;
}

class FlatRate implements ShippingStrategy {
  constructor(private readonly fee: Money) {}
  quote(): Money {
    return this.fee;
  }
}

class ByWeight implements ShippingStrategy {
  constructor(private readonly perKg: Money) {}
  quote(order: Order): Money {
    return Math.ceil(order.weightKg) * this.perKg;
  }
}

class FreeOverThreshold implements ShippingStrategy {
  constructor(
    private readonly threshold: Money,
    private readonly fallback: ShippingStrategy,
  ) {}
  quote(order: Order): Money {
    return order.subtotal >= this.threshold ? 0 : this.fallback.quote(order);
  }
}

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

  setShipping(shipping: ShippingStrategy): void {
    this.shipping = shipping;
  }

  total(order: Order): Money {
    return order.subtotal + this.shipping.quote(order);
  }
}

// Selection lives at the edge, not inside Checkout.
const strategies: Record<string, ShippingStrategy> = {
  flat: new FlatRate(499),
  weight: new ByWeight(150),
  promo: new FreeOverThreshold(5000, new FlatRate(499)),
};

const checkout = new Checkout(strategies["promo"]);
console.log(checkout.total({ subtotal: 6200, weightKg: 3.2 })); // 6200
console.log(checkout.total({ subtotal: 1800, weightKg: 3.2 })); // 2299

Trade-offs

Gains

  • New behaviors are new code, and the context stays closed to modification.
  • Each algorithm is unit-testable alone.
  • Runtime swapping without conditionals.
  • Strategies compose: FreeOverThreshold wraps another strategy.

Costs

  • More types, and the reader has to jump to find the active behavior.
  • Clients must know the strategies exist in order to pick one; the selection logic has to live somewhere.
  • A wide strategy interface forces every variant to implement methods it doesn't need.
  • Overkill when a function parameter would do.

Relations

  • State has the same shape, but states know about each other and trigger transitions. Strategies are independent and chosen from outside.
  • Template Method varies steps through inheritance at compile time. Strategy varies the whole algorithm through composition at runtime.
  • Command also wraps behavior in an object, but to queue, log or undo an action, not to pick between ways of computing something.
  • Decorator changes an object's skin by wrapping it. Strategy changes its guts by replacing an internal part.
  • Bridge uses the same delegation structurally, to keep two whole hierarchies independent.

Agent checklist

  • There are at least three real variants, or a third is concretely planned. Otherwise keep the conditional.
  • If the language has first-class functions, you considered a function type before writing an interface.
  • The strategy interface takes inputs as arguments rather than a reference to the whole context.
  • Selection (which strategy) lives at the composition root, config or a lookup map, not inside the context.
  • Each concrete strategy has its own test, and the original behavior is preserved, verified by running the existing tests.

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.