Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Strategy

The algorithm as a parameter — and why in most languages it is now just a function.


A checkout has to add shipping. The rule depends on the carrier: some charge a flat rate, some charge by weight, some waive it above a threshold.

What people write first

class Checkout {
total(order: Order, carrier: string): number {
let shipping: number;
if (carrier === 'flat') shipping = 5;
else if (carrier === 'weight') shipping = Math.ceil(order.weightKg) * 2.5;
else if (carrier === 'free') shipping = order.subtotal >= 50 ? 0 : 7;
else throw new Error(`unknown carrier: ${carrier}`);
return order.subtotal + shipping;
}
}

This is fine. Genuinely — for three rules that never change, stop here.

It becomes a problem when the rules keep coming. Each new carrier means editing a class that is already correct and already tested, and the branch tends to reappear: once in total, again in estimate, again in the checkout summary. Now adding a carrier means finding all three.

The pattern

Make the varying part an object, and hand it in.

interface ShippingRule {
cost(order: Order): number;
}
class FlatRate implements ShippingRule {
cost() { return 5; }
}
class ByWeight implements ShippingRule {
cost(order: Order) { return Math.ceil(order.weightKg) * 2.5; }
}
class Checkout {
constructor(private readonly shipping: ShippingRule) {}
total(order: Order) {
return order.subtotal + this.shipping.cost(order);
}
}

Switch the rule and watch only the rule change:

Shipping strategy
// the only thing that varies
cost(order) { return 5; }
subtotal
£84.00
shipping
£5.00
total
£89.00

The Checkout class is identical in all three. It never learns which rule it is using, which is the entire point — a fourth carrier is a new object, not a new branch inside code that already works.

In this language, it is a function

The interface has one method. In any language with first-class functions, that is a function type, and the whole class hierarchy collapses:

type ShippingRule = (order: Order) => number;
const flatRate: ShippingRule = () => 5;
const byWeight: ShippingRule = (o) => Math.ceil(o.weightKg) * 2.5;
const freeOver = (limit: number): ShippingRule => (o) => (o.subtotal >= limit ? 0 : 7);
class Checkout {
constructor(private readonly shipping: ShippingRule) {}
total(order: Order) { return order.subtotal + this.shipping(order); }
}
new Checkout(freeOver(50)).total(order);

Same pattern, same benefit, none of the ceremony. freeOver is a parameterised strategy — the thing that would otherwise need a constructor and a private field.

Reach for the class form when the strategy needs more than one method, when it holds meaningful state, or when your codebase’s conventions expect it. A single function is otherwise better.

Where you already use it

array.sort(comparator) is Strategy. So is every middleware function, every React render prop, every onClick handler, and every retry policy passed to an HTTP client. You have been writing it for years.

Neighbours

State has the same structure — an object holding varying behaviour — and a different intent. Strategies are chosen by the caller and do not know about each other; states swap themselves in response to events.

Template Method solves the same “one algorithm, varying steps” problem with inheritance rather than composition: the steps are overridden methods instead of an injected object. Strategy composes, so it can change at runtime; Template Method is fixed at compile time.