Strategy Pattern
The strategy pattern lets you define a family of interchangeable algorithms, encapsulate each one, and select which to use at runtime. Instead of hard-coding behavior inside a sprawling if/else or switch, you treat each variation as a self-contained unit that conforms to a shared contract. Because functions are first-class values in JavaScript, the pattern is unusually clean here: a “strategy” is often just a function, and a collection of them is a plain object. The payoff is code that stays open to new behaviors without rewriting the code that consumes them.
The problem: growing conditionals
Picture a checkout that computes shipping cost. The naive version branches on a string, and every new carrier means another else if and a re-test of the whole function.
function shippingCost(method, weightKg) {
if (method === "standard") {
return 5 + weightKg * 0.5;
} else if (method === "express") {
return 12 + weightKg * 0.9;
} else if (method === "overnight") {
return 25 + weightKg * 1.5;
}
throw new Error(`Unknown method: ${method}`);
}
This violates the open/closed principle: the function must be edited for every addition, the branches share no enforced shape, and the logic is impossible to reuse or test in isolation.
Strategies as first-class functions
The simplest strategy implementation is a map from a key to a function. Each function is one algorithm. Selection becomes a lookup instead of a branch.
const shippingStrategies = {
standard: (weightKg) => 5 + weightKg * 0.5,
express: (weightKg) => 12 + weightKg * 0.9,
overnight: (weightKg) => 25 + weightKg * 1.5,
};
function shippingCost(method, weightKg) {
const strategy = shippingStrategies[method];
if (!strategy) throw new Error(`Unknown method: ${method}`);
return strategy(weightKg);
}
console.log(shippingCost("express", 2)); // 13.8
Output:
13.8
Adding a carrier is now a one-line addition to the map, and the shippingCost function never changes. Each strategy is a pure function you can unit-test directly.
Validate the incoming key before calling. An unknown method should fail loudly rather than silently doing nothing — a missing strategy is a programmer error, not a runtime fallback.
Choosing a strategy at runtime
Because strategies are values, you can pick one based on anything available at runtime — user input, configuration, feature flags, or A/B test buckets — and pass it around like any other argument.
function makeShipper(strategy) {
return (order) => ({
...order,
cost: strategy(order.weightKg),
});
}
const userPref = "overnight";
const ship = makeShipper(shippingStrategies[userPref]);
console.log(ship({ id: 42, weightKg: 3 }));
Output:
{ id: 42, weightKg: 3, cost: 29.5 }
You can also supply a default and merge user-supplied strategies, which keeps the consumer fully decoupled from the concrete algorithms.
Object strategies for richer behavior
When a strategy needs more than one operation or some shared setup, model it as an object that satisfies a known shape (a duck-typed interface). Sorting comparators and validation rules are common cases.
const sortStrategies = {
byName: (a, b) => a.name.localeCompare(b.name),
byPriceAsc: (a, b) => a.price - b.price,
byPriceDesc: (a, b) => b.price - a.price,
};
function sortProducts(products, strategyKey) {
const compare = sortStrategies[strategyKey] ?? sortStrategies.byName;
return [...products].sort(compare);
}
const items = [
{ name: "Mug", price: 9 },
{ name: "Pen", price: 3 },
];
console.log(sortProducts(items, "byPriceAsc"));
Output:
[ { name: 'Pen', price: 3 }, { name: 'Mug', price: 9 } ]
Note that sortProducts copies the array before sorting, so the strategy stays free of side effects on the caller’s data.
When to reach for it
| Situation | Strategy pattern? |
|---|---|
| Several variants of one operation chosen at runtime | Yes — a strategy map fits perfectly |
| Each variant is a tiny one-off branch with no reuse | Probably overkill; a switch is fine |
| Behaviors added by plugins or config without touching core | Yes — register strategies dynamically |
| The “algorithm” also depends on object lifecycle/transitions | Consider the state pattern instead |
The strategy pattern and the state pattern look similar because both swap behavior via an object. The distinction: strategies are chosen by the client and are interchangeable for the same task, while states are driven by internal transitions and represent where an object is in its lifecycle.
Best practices
- Keep strategies pure where possible — a function of inputs to outputs is the easiest to test, compose, and reason about.
- Enforce a single, documented contract (signature or shape) so any strategy is a drop-in replacement for another.
- Look up strategies from a map keyed by a stable identifier rather than nesting conditionals.
- Always handle the unknown-key case explicitly: throw for programmer errors, or fall back with
??only when a sensible default truly exists. - Let strategies be registered from outside (config, plugins, dependency injection) so the consuming code stays closed to modification.
- Don’t reach for the pattern when there’s a single, stable branch — a plain conditional is clearer than ceremony.