Skip to main content
Back to module
Software Engineering Practices · Design

Common design patterns

Reading1h 5mLesson 2 of 3 in this module

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

ProblemPattern
Several interchangeable rules, chosen at run timeStrategy
An existing interface does not match what your code expectsAdapter
You need to add behaviour around a function (logging, caching) without changing itDecorator (a wrapper function)
Many parts of the app need to react when something happensObserver (events, addEventListener)

Common mistakes

  • Patterns for their own sake. Two if branches 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_name anywhere, 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.

Also in this lesson: