codejump

AcademyDesign Patterns

Strategy pattern

Swapping an algorithm without touching the class that uses it, the interface every strategy must implement even for a one-off, and reaching for it before a second case exists.

6 min readUpdated

3 exercises

Waiting at the end of this page.

Strategy answers the most direct question among the eleven behavioral patterns: how to swap an algorithm without rewriting the class that uses it.

A Strategy pulls "how do I do this" out of a class and into a separate, interchangeable piece, a function or an object with one method, that the class calls without caring which one it got handed. The class that uses the strategy never grows an if for every variant; it just calls whatever it was given.

The problem it solves

You sell subscriptions and compute a discount. There is a student discount, a yearly-plan discount, a coupon code, and a Black Friday sale. A first version puts everything in one function:

TypeScript
function price(base: number, type: string): number {
  if (type === 'student') return base * 0.5;
  if (type === 'yearly') return base * 0.8;
  if (type === 'blackfriday') return base * 0.7;
  return base;
}
  • Every new rule edits the same function. It grows, and each edit risks the others.
  • Rules cannot be tested alone. They are tangled with the branching.
  • The choice is fixed in code. You cannot pass in a rule from configuration or from a test.

The fix is to make each rule its own function and let the caller pick which one to hand in.

Structure

The context calls whichever strategy it was given. Swapping one for another needs no change to the context
The context calls whichever strategy it was given. Swapping one for another needs no change to the context

Three parts:

  • The strategy interface: one operation, such as (arr) => sorted.
  • Concrete strategies: the interchangeable implementations (ascending, descending).
  • The context (Sorter): holds a strategy and calls it.
class Sorter {
  constructor(strategy) {
    this.strategy = strategy;
  }
  sort(arr) {
    return this.strategy(arr);
  }
}

const ascending = (arr) => [...arr].sort((a, b) => a - b);
const descending = (arr) => [...arr].sort((a, b) => b - a);

// new Sorter(ascending).sort([3, 1, 2])  → [1, 2, 3]
// new Sorter(descending).sort([3, 1, 2]) → [3, 2, 1]

Sorter has no idea which strategy it holds, and never branches on it. Adding byLength means writing one function, and nothing else changes.

Where it is used in real systems

The login route asks a strategy to authenticate. Local, Google, GitHub and JWT all plug into the same interface
The login route asks a strategy to authenticate. Local, Google, GitHub and JWT all plug into the same interface
Use caseThe interchangeable algorithms
AuthenticationPassport strategies: local, Google, GitHub, JWT.
Pricing and discountsStudent, yearly, coupon, sale.
Sorting and comparisonThe compare function passed to Array.sort.
Payment processingCard, bank transfer, wallet.
ValidationDifferent rules for different countries or plans.
Compression and encodinggzip, brotli or none, based on the client.
CachingLeast recently used, time based, or none.
Routing and retriesFixed delay, exponential backoff, or no retry.

The clue is a switch or a chain of if statements on a "type" that picks between ways of doing the same job.

Strategies you already use

  • `array.sort(compareFn)`. The compare function is a strategy.
  • `array.map`, `filter` and `reduce` take the varying part as a function.
  • Passport.js. Each login method is a strategy plugged into the same authenticate call.
  • Retry libraries. Backoff policies passed as options.
  • React render props and hooks. A component that takes a function deciding how to render or sort.
  • Prisma and ORMs choosing a dialect, or bundlers choosing a loader per file type.

Strategy in JavaScript is often just a function

In languages with first-class functions, you rarely need a class. Pass the function. A map of named strategies keeps it tidy and open to extension:

TypeScript
type Discount = (base: number) => number;

const discounts: Record<string, Discount> = {
  student: (base) => base * 0.5,
  yearly: (base) => base * 0.8,
  none: (base) => base,
};

function price(base: number, kind: keyof typeof discounts): number {
  return discounts[kind]?.(base) ?? base;
}

Reach for a class when a strategy carries state or several related methods. Otherwise, a function is enough.

Strategy, State and Template Method

Who picksChanges over timeWhere the variation lives
StrategyThe clientRarelyA separate object passed in
StateThe object itselfOftenA separate object that replaces itself
Template MethodThe class hierarchyNeverA subclass overriding a step

If you are choosing how something is done, and can choose again from outside, it is Strategy.

When not to use it

  • Only two variants that will never grow. An if is simpler.
  • Callers must know the details. If picking a strategy requires understanding how each works, the choice is not really encapsulated.
  • Too many tiny strategies. A file per one-line function is noise. Keep small ones together.
  • The variation is data, not behavior. If only a number changes (a rate of 0.2 instead of 0.5), pass the number.

What interviewers ask

  • "What is the Strategy pattern?" Defining a family of interchangeable algorithms and letting the client choose one at runtime.
  • "How does it remove `if/else`?" The branching moves to where the strategy is chosen, and the code that uses it just calls it.
  • "Strategy versus State?" Same shape. Strategy is chosen from outside and stays. State changes itself over time.
  • "Give a JavaScript example." The Array.sort compare function, or Passport strategies.
  • "How does it help testing?" Each strategy is tested alone, and the context is tested with a fake strategy.

The mistake to avoid

A strategy chosen by an if inside the context. If Sorter.sort starts with if (this.strategy === ascending), the context knows about specific strategies again and the pattern has been undone. The context should only call the strategy. Any special case belongs in the strategy itself.

The mechanism above, passing in the behavior that varies instead of branching on it, is the same in JavaScript, Python or Java. Only the syntax changes.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.