Beginner12 min readDesign Patternslive prototype

Strategy

Define a family of interchangeable algorithms behind one interface, so the client can pick and swap them at runtime. A maps app computes πŸš— drive, 🚴 bike and 🚢 walk routes in completely different ways β€” but the map just calls `route(A, B)` on whichever `RouteStrategy` it currently holds. Swap the strategy and the *same call* produces a different route, with no if/else anywhere.

The idea

What it is

Strategy answers a question every codebase eventually hits: what do you do when there are several ways to perform the same task? A maps app can take you from A to B by car, by bike or on foot β€” same start, same destination, three totally different route computations. The naive answer is one giant method with an if (mode === "drive") … else if (mode === "bike") … else … β€” and that method grows, tangles and breaks a little more with every new travel mode. Strategy takes the opposite route: pull each algorithm out into its own small class, make them all implement one interface (RouteStrategy with a single buildRoute(A, B) method), and let the map hold whichever one is currently selected.

The payoff is that the client code never changes. The map object doesn't know highways from bike lanes β€” it holds one strategy reference and calls buildRoute(A, B) on it. Tap the 🚢 chip and the app just swaps which object sits behind that reference; the call site is untouched. New travel mode next quarter (πŸ›΄ scooter)? Write one new class that implements RouteStrategy β€” you never reopen the map, and you never re-test the three algorithms that already worked. Alongside [[singleton]], this is one of the two most-asked patterns in interviews, because it's the cleanest demonstration of composition, polymorphism and the open–closed principle working together.

The one sentence to remember

Strategy defines a family of interchangeable algorithms, puts each behind one common interface, and lets the client pick or swap the algorithm at runtime β€” the code that uses the algorithm never changes, only the object plugged into it does.

You've already used it

Every time you pass a comparator to a sort β€” routes.sort((a, b) => a.km - b.km) β€” you're using Strategy. The sort is the context (the stable skeleton), and the callback is the interchangeable algorithm you plug in. In functional languages a strategy is often just a passed-in function.

Mechanics

How it works

The three participants

A textbook Strategy has three moving parts, and each is small:

  • The Strategy interface β€” one contract every algorithm signs, e.g. RouteStrategy with a single method buildRoute(from, to). This is [[program-to-interfaces]] in its purest form: the client depends on this contract and nothing else.
  • Concrete strategies β€” one small class per algorithm: DriveStrategy, BikeStrategy, WalkStrategy. Each hides its own details (highway graphs, bike-lane maps, footpaths) behind the same method signature. They're interchangeable precisely because they're indistinguishable through the interface.
  • The Context β€” the class that needs the work done (the MapApp). It holds one strategy reference, receives it via the constructor or a setter (that's constructor/setter injection β€” see [[dependency-injection-and-ioc]]), and delegates: route(A, B) just calls this.strategy.buildRoute(A, B). The context never implements an algorithm and never inspects which one it holds.

The smell it cures: the algorithm-choosing conditional

Strategy exists to kill one specific smell: a growing `if/else` or `switch` that picks an algorithm inline. One method that branches on a mode string, with each branch containing a whole algorithm, has three compounding problems: every new variant means editing a class that already worked (a direct [[open-closed]] violation), the variants can't be tested in isolation because they share locals and state, and the conditional inevitably gets copy-pasted to other places that need the same choice. Strategy replaces the branch point with polymorphism β€” the 'decision' happens once, when someone plugs a strategy in; after that, calling the method is the dispatch.

typescript
interface RouteStrategy {                    // the one shared contract
  buildRoute(from: string, to: string): string;
}

class DriveStrategy implements RouteStrategy {
  buildRoute(a: string, b: string) { return `${a}→highways→${b}`; }
}
class WalkStrategy implements RouteStrategy {
  buildRoute(a: string, b: string) { return `${a}→park path→${b}`; }
}

class MapApp {                                // the context
  constructor(private strategy: RouteStrategy) {}   // injected in
  setStrategy(s: RouteStrategy) { this.strategy = s; }  // swap at runtime
  route(a: string, b: string) {
    return this.strategy.buildRoute(a, b);    // delegate β€” no if/else
  }
}

const map = new MapApp(new DriveStrategy());
map.route("A", "B");                 // drive route
map.setStrategy(new WalkStrategy()); // user taps 🚢
map.route("A", "B");                 // SAME call, different algorithm

The modern shortcut: a strategy is often just a function

When a strategy interface has exactly one method, a whole class can be overkill. In JavaScript/TypeScript, Python, and modern Java/C++, you can pass a function or lambda instead: routes.sort((a, b) => a.time - b.time) is Strategy with zero ceremony β€” the comparator is the strategy. Reach for the class version when strategies carry their own state or config (API keys, tuning parameters), or when the family is big enough to deserve names.

Strategy vs State: same shape, different driver

Strategy's structure β€” context delegating to a swappable object behind an interface β€” is identical to the [[state]] pattern, and interviewers love asking the difference. It's about who does the swapping and why. In Strategy, the client picks the algorithm (you tap 🚴 in the maps app), the strategies don't know each other exist, and the chosen strategy rarely changes mid-task. In State, the objects represent modes of being (an order that's Placed, Paid, Shipped), they typically know about each other and trigger their own transitions β€” paying an order moves it to Paid without any client choosing. One sentence: Strategy is chosen from outside; State switches itself from inside.

Where you'll meet it in the wild

  • Sort comparators β€” Array.sort(cmp), Java's Comparator, C++'s std::sort predicate: the classic function-as-strategy.
  • Payment methods β€” checkout calls pay(amount) on a PaymentStrategy; card, UPI, wallet and PayPal each implement it their own way.
  • Compression codecs β€” an archiver holds a CompressionStrategy (zip, gzip, zstd) and calls compress(bytes) without caring which.
  • Pricing & discount rules β€” seasonal, coupon, loyalty-tier discounts as interchangeable DiscountStrategy objects the cart applies.
  • Auth providers β€” login flows swap AuthStrategy implementations (password, OAuth, magic link); libraries like Passport.js literally call them strategies.
  • Route planning β€” the running example: navigation apps swap routing algorithms per travel mode over the same buildRoute call.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation β€” no setup, no install. Play with it as you read.

About this simulation

A tiny maps app routing from A to B. Pick a strategy chip β€” πŸš— Drive, 🚴 Bike or 🚢 Walk β€” then press β–Ά route(). Each strategy draws a completely different path across the same city: Drive hugs the highway around the edge (fast but long), Bike follows the riverside lanes, Walk cuts straight through the park (short but slow). The one thing that never changes is the mono card reading map.route(A β†’ B) β€” that's the client code, and it stays byte-for-byte identical while the algorithm behind it swaps. Switch chips and re-route a few times: same call, different algorithm, different result. That's the whole pattern.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Run the same call three ways

In the prototype, leave πŸš— Drive selected and press β–Ά route(). A long blue path draws along the highway ring β€” 12 min Β· 8.4 km. Now tap 🚴 Bike, press β–Ά route() again: the drive path vanishes and a riverside route replaces it. Tap 🚢 Walk and route once more β€” a short green path cuts diagonally through the park, 58 min Β· 4.7 km. Three totally different algorithms, three different results.

try 02

Stare at the context card

While you swap chips, watch the mono card that reads map.route(A β†’ B) β€” it never changes, which is why it carries the little 'client code: unchanged' chip. That card is the whole point of the pattern: the context makes one polymorphic call, and the strategy chips only change which object receives it. No branch in the client ever asks 'which mode am I in?'.

try 03

Swap mid-flight, then reset

Press β–Ά route() and, while the path is still drawing, click a different chip and route again β€” the old path is replaced instantly, because the map holds exactly ONE strategy at a time (not three tangled branches). Finish with β†Ί Reset: the map clears, but the selected chip stays β€” the strategy is state the context holds, not something rebuilt per call.

In practice

When to use it β€” and what trips people up

Reach for Strategy when…

  • You have many variants of one algorithm β€” several ways to route, price, compress, sort or authenticate, all doing 'the same job' differently. That's the family Strategy is built for.
  • The choice happens at runtime β€” the user taps a travel-mode chip, a config flag picks a codec, an A/B test assigns a pricing rule. Swapping an object is trivial; rewriting a conditional at runtime is impossible.
  • You want each algorithm isolated and testable β€” every strategy is a small class you can unit-test alone, hand a fake to, or tune without touching its siblings or the context.
  • A conditional that picks algorithms keeps growing β€” an if/else chain over modes that every new feature edits is the flashing sign; Strategy replaces the branches with one polymorphic call and makes new variants pure additions ([[open-closed]]).

Skip it when…

  • There's one algorithm and it never varies β€” an interface, three files and an injection point to wrap a single stable computation is pure ceremony. Inline it and move on; extract a strategy the day a second variant actually appears.
  • The variants are trivially small β€” if each 'algorithm' is one expression (a comparator, a formatter), pass a function argument instead of building the class version. Same pattern, a tenth of the code.

What it gives you

  • Swap algorithms at runtime β€” the context holds a reference, so changing behaviour is one assignment, not a code change.
  • Open–closed wins: a new variant is a new class that implements the interface β€” existing, tested strategies and the context stay untouched.
  • Each algorithm is isolated β€” its data structures and helpers live in its own class, unit-testable alone and easy to mock in the context's tests.
  • Kills duplicated algorithm-choosing conditionals β€” the mode decision happens once (where the strategy is injected), instead of being re-branched at every call site.

Common mistakes

  • More classes and indirection β€” a family of three one-line algorithms becomes an interface plus three files; overkill when a function argument would do.
  • Clients must know the strategies to choose one β€” something (UI, config, factory) still has to map 'user tapped 🚴' to new BikeStrategy(); the decision moves, it doesn't vanish.
  • The shared interface can pinch β€” all strategies must fit one signature, so a variant that needs extra inputs forces either a wider interface or config passed into its constructor.
  • Harder to read for newcomers than a plain conditional β€” behaviour is spread across classes and picked at runtime, so 'which code actually ran?' takes one more hop to answer.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// 1) The Strategy interface β€” one contract for the whole family.
interface RouteStrategy {
  buildRoute(from: string, to: string): string;
}

// 2) Concrete strategies β€” each computes the route its own way.
class DriveStrategy implements RouteStrategy {
  buildRoute(from: string, to: string): string {
    return `${from} β†’ highways β†’ ${to}  (12 min Β· 8.4 km)`;
  }
}
class BikeStrategy implements RouteStrategy {
  buildRoute(from: string, to: string): string {
    return `${from} β†’ river lanes β†’ ${to}  (26 min Β· 6.1 km)`;
  }
}
class WalkStrategy implements RouteStrategy {
  buildRoute(from: string, to: string): string {
    return `${from} β†’ park path β†’ ${to}  (58 min Β· 4.7 km)`;
  }
}

// 3) The Context β€” holds ONE strategy, delegates, never branches.
class MapApp {
  constructor(private strategy: RouteStrategy) {}  // injected in
  setStrategy(s: RouteStrategy): void { this.strategy = s; }
  route(from: string, to: string): string {
    return this.strategy.buildRoute(from, to);     // polymorphic call
  }
}

const app = new MapApp(new DriveStrategy());
console.log(app.route("A", "B"));   // drive route
app.setStrategy(new WalkStrategy());  // user taps 🚢
console.log(app.route("A", "B"));   // SAME call β†’ walking route

// The lambda shortcut: a one-method strategy is just a function.
const distances = [8.4, 6.1, 4.7];
distances.sort((a, b) => a - b);    // the comparator IS a strategy

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 06

What is the intent of the Strategy pattern?

question 02 / 06

In the maps-app example, what is the role of the Context (the MapApp class)?

question 03 / 06

Your pricing method is a growing switch over discount types, and every new discount edits that method. How does Strategy fix this?

question 04 / 06

Strategy and State have nearly identical structure. What's the key difference?

question 05 / 06

You call routes.sort((a, b) => a.km - b.km). How does this relate to the Strategy pattern?

question 06 / 06

When is applying the Strategy pattern probably a mistake?

0/6 answered