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.
RouteStrategywith a single methodbuildRoute(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 callsthis.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.
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 algorithmThe 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'sComparator, C++'sstd::sortpredicate: the classic function-as-strategy. - Payment methods β checkout calls
pay(amount)on aPaymentStrategy; card, UPI, wallet and PayPal each implement it their own way. - Compression codecs β an archiver holds a
CompressionStrategy(zip, gzip, zstd) and callscompress(bytes)without caring which. - Pricing & discount rules β seasonal, coupon, loyalty-tier discounts as interchangeable
DiscountStrategyobjects the cart applies. - Auth providers β login flows swap
AuthStrategyimplementations (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
buildRoutecall.
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.
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.
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?'.
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/elsechain 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 strategyReferences & further reading
5 sources- Articlerefactoring.guru
Strategy β Refactoring.Guru
The clearest illustrated walkthrough β a navigation-app example almost identical to this lesson's, structure diagrams, and 'how to implement' steps. Best first read.
- Booken.wikipedia.org
Design Patterns: Elements of Reusable Object-Oriented Software β Gamma, Helm, Johnson, Vlissides
The original 'Gang of Four' book that catalogued Strategy. The primary source for its intent: 'define a family of algorithms, encapsulate each one, and make them interchangeable.'
- Bookoreilly.com
Head First Design Patterns β Freeman & Robson (Chapter 1: the duck simulator)
Famously opens the whole book with Strategy: ducks whose fly and quack behaviours are swappable objects. The gentlest deep-dive on 'favor composition over inheritance.'
- Articlebaeldung.com
The Strategy Pattern in Java β Baeldung
Practical Java treatment, including how Java 8 lambdas collapse one-method strategies into function references β the modern shortcut this lesson mentions.
- Docsen.wikipedia.org
Strategy pattern β Wikipedia
Concise reference with UML, multi-language examples, and the relationship to the openβclosed principle and to function-as-argument styles.
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