Use the strategy pattern to swap rules at run time and the adapter pattern to wrap an external interface.
Why it matters
A design pattern is a named, reusable shape for solving a problem that keeps coming up. The value of the name is communication: when a reviewer writes "this switch statement wants to be a strategy", everyone on the team knows what change is being suggested. The classic catalogue (the 1994 "Gang of Four" book) lists 23 patterns written for class-based languages. In JavaScript many of them shrink to a few lines because functions are values. This lesson covers two you will meet constantly in real code: strategy and adapter.
Strategy: swap the algorithm, keep the caller
Use a strategy when you have several interchangeable ways to do one job and the choice is made at run time. A typical smell is a growing switch:
function shippingCost(order, method) {
switch (method) {
case "standard": return 5;
case "express": return 5 + order.weightKg * 2;
case "pickup": return 0;
default: throw new Error("Unknown method");
}
}
Every new shipping option edits this function, and every rule lives in one place that everybody touches. With a strategy, each rule becomes its own function and the caller looks it up:
const shippingStrategies = {
standard: () => 5,
express: (order) => 5 + order.weightKg * 2,
pickup: () => 0,
};
function shippingCost(order, method) {
const strategy = shippingStrategies[method];
if (!strategy) throw new Error("Unknown shipping method: " + method);
return strategy(order);
}
Adding "overnight" is now a new entry, not an edit to existing logic (this is the open/closed principle in practice). Each strategy is a pure function you can test alone. You have used this pattern already if you have passed a compare function to array.sort((a, b) => a - b): the sort algorithm stays the same while the comparison strategy changes.
One caution: an object lookup like shippingStrategies[method] will also find inherited keys such as "toString". When the key comes from user input, check with Object.hasOwn(shippingStrategies, method), or build the map with Object.create(null) or a Map.
Adapter: make one interface look like another
Use an adapter when you have code that expects one shape and a dependency that provides a different one. Instead of changing every caller, or forking the library, you write a thin translation layer.
Suppose your app works with users shaped like { id, name, email }, and you integrate an old system that returns:
const legacy = { user_id: "42", first_name: "Ada", last_name: "Lovelace", mail: "ADA@EXAMPLE.COM" };
Write one function that converts at the boundary:
function adaptLegacyUser(raw) {
return {
id: Number(raw.user_id),
name: (raw.first_name + " " + raw.last_name).trim(),
email: raw.mail.toLowerCase(),
};
}
The rest of your code never learns that the legacy format exists. If the vendor changes field names, you fix one function. Adapters are also how you wrap third-party services, for example a payment provider whose method is createCharge(cents) behind your own pay(amountDollars) interface. That gives you a natural seam for tests: in a test you swap in a fake object with the same interface.
How to choose
| Problem | Pattern |
|---|---|
| Several interchangeable rules, chosen at run time | Strategy |
| An existing interface does not match what your code expects | Adapter |
| You need to add behaviour around a function (logging, caching) without changing it | Decorator (a wrapper function) |
| Many parts of the app need to react when something happens | Observer (events, addEventListener) |
Common mistakes
- Patterns for their own sake. Two
ifbranches do not need a strategy map. Reach for the pattern when the third or fourth case arrives, or when the rules need separate tests. - Leaky adapters. If callers still check
raw.first_nameanywhere, the adapter is not doing its job. Convert once, at the edge. - Adapters that add business logic. An adapter translates. Discount rules belong in your domain code, not in the layer that renames fields.
- Silently ignoring unknown strategies. Throw a clear error (or return a well-defined default) when the name is not found.
Recap
- A pattern is a shared name for a proven shape; its biggest benefit is communication.
- Strategy: put interchangeable rules behind one function signature and pick one at run time.
- Adapter: translate an external shape into the shape your code expects, at the boundary.
- In JavaScript both are usually plain functions and objects, not class hierarchies.
Try it
In the Do tab, replace a discount switch with a strategy map and write an adapter for a payment provider.