Facebook Pixel

Strategy Pattern

The branching algorithm problem

A checkout service calculates shipping cost. It starts with one carrier, so the logic lives inline. Then the product team adds a second carrier — a branch appears. Then a third, then a fourth for international orders, then a special rate for Prime members. The method that once fit in ten lines now sprawls across a hundred, and every new carrier requires touching the middle of an existing function:

1# Bad: every new carrier forces a change inside this method.
2def calculate_shipping(order, carrier: str) -> float:
3    if carrier == "STANDARD":
4        return order.weight_kg * 3.50
5    elif carrier == "EXPRESS":
6        return order.weight_kg * 7.00 + 5.00
7    elif carrier == "OVERNIGHT":
8        return 25.00 + order.weight_kg * 10.00
9    elif carrier == "INTERNATIONAL":
10        return order.weight_kg * 15.00 + order.declared_value * 0.02
11    else:
12        raise ValueError(f"unknown carrier: {carrier}")

Adding a fifth carrier means editing this function, re-reading its logic, and risking a regression in all four existing branches. This is the signal for the Strategy pattern: a block of if/else or switch selecting an interchangeable algorithm by a type tag.

How it works

Extract each branch into its own class. Give every class the same single-method interface — calculate(order) in this case. The context (ShippingCalculator) holds a reference to whichever strategy was injected and calls it without knowing which one it is. To add a carrier, you add one class; nothing else changes.


The context delegates without branching; the caller decides which strategy applies. The pattern applies to any system where a type-tag parameter selects between interchangeable algorithms: payment processors, discount rules, sort orders, search ranking.

At runtime the context forwards the call to whichever strategy object it currently holds, with no conditional logic inside ShippingCalculator itself.

Good: one class per algorithm, swapped at runtime

1from __future__ import annotations
2from abc import ABC, abstractmethod
3from dataclasses import dataclass
4
5@dataclass
6class Order:
7    weight_kg: float
8    declared_value: float = 0.0
9
10class ShippingStrategy(ABC):
11    @abstractmethod
12    def calculate(self, order: Order) -> float: ...
13
14class StandardShipping(ShippingStrategy):
15    def calculate(self, order: Order) -> float:
16        return order.weight_kg * 3.50
17
18class ExpressShipping(ShippingStrategy):
19    def calculate(self, order: Order) -> float:
20        return order.weight_kg * 7.00 + 5.00
21
22class OvernightShipping(ShippingStrategy):
23    def calculate(self, order: Order) -> float:
24        return 25.00 + order.weight_kg * 10.00
25
26class InternationalShipping(ShippingStrategy):
27    def calculate(self, order: Order) -> float:
28        return order.weight_kg * 15.00 + order.declared_value * 0.02
29
30class ShippingCalculator:
31    def __init__(self, strategy: ShippingStrategy) -> None:
32        self._strategy = strategy
33
34    def set_strategy(self, strategy: ShippingStrategy) -> None:
35        self._strategy = strategy
36
37    def calculate(self, order: Order) -> float:
38        return self._strategy.calculate(order)
39
40# Adding a fifth carrier means adding one class, touching nothing else.
41order = Order(weight_kg=2.5, declared_value=200.0)
42calc = ShippingCalculator(StandardShipping())
43print(calc.calculate(order))  # 8.75
44
45calc.set_strategy(InternationalShipping())
46print(calc.calculate(order))  # 41.5

Recognizing the pattern

The signal for Strategy is a method with a type-tag parameter and a branch for each variant. When that shape appears, the remedy is to extract each branch into a class that implements a common interface, then inject the chosen implementation into the context.

Common domains where this arises: payment gateways (multiple providers), discount calculators (seasonal, loyalty, coupon codes), and sort orders (price ascending, rating descending, newest first).

Stateless strategies and reuse

When a strategy holds no mutable state — no counters, no caches, no fields that change between calls — a single instance can be shared across all callers instead of constructing a new one each time. StandardShipping above stores nothing; the same instance can serve every ShippingCalculator in the application. Store the single immutable instance in a map keyed by carrier name, or hold it as a module-level constant, and reuse it freely.

The heuristic for when to apply this: if the strategy object has no fields, or all its fields are set once at construction and never mutated, treat it as a shared singleton. If it accumulates state across calls, each caller needs its own instance.

When to introduce Strategy vs. leaving a conditional

A useful heuristic: reach for Strategy when the same conditional has been edited two or more times to add a new branch, or when the branching logic is the only thing that differs between otherwise-identical modules. A conditional that has existed unchanged for a year and serves two stable cases is not a problem to solve. The pattern pays for itself when branches arrive at different times from different sources — each new carrier, payment provider, or discount rule comes from a separate team or ticket. Strategy gives each one an isolated class to work in without touching the others.

When NOT to use this

Strategy adds a layer of indirection — a strategy object must be instantiated and injected, and the calling code must know which variant it wants. When there is only one algorithm and no evidence that a second will be needed, that indirection is unnecessary overhead. A concrete class with the logic inline is simpler and easier to read.

Avoid it also when the variants share significant logic. If three of your four shipping calculators do the same weight-based math with only a multiplier different, pulling them into separate classes duplicates that logic. A better approach may be a single class parameterized by the multiplier, or a template-method structure where the shared logic lives in a base class.

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro