Strategy
A shopping cart needs to price items differently depending on the customer — regular price, bulk discount, clearance markdown — but "how a cart works" shouldn't have to change every time the business invents a new pricing scheme. The Strategy pattern handles this by pulling each interchangeable algorithm out into its own class, so the context just holds a reference to whichever one it was handed and calls it, with zero knowledge of how it actually works.
⚠️ The Problem
Imagine a ShoppingCart.calculateTotal() method that needs to support regular
pricing, a bulk discount for orders over a certain quantity, and a clearance markdown — and
picks between them with a pricingType flag: if (pricingType === "bulk") {
... } else if (pricingType === "clearance") { ... } else { ... }. Every time the business
adds a new pricing scheme — a loyalty-member discount, a holiday sale — someone has to go back
into this one central method and add another branch. The method grows forever, mixes multiple
unrelated algorithms' logic together in one place, and testing one pricing scheme in isolation
means wading through all the others' code too.
✅ The Solution
Extract each pricing scheme into its own class implementing a common
PricingStrategy interface — RegularPricing,
BulkDiscountPricing, ClearancePricing — each with a method like
calculate(items). ShoppingCart holds a reference to whichever strategy
object was passed in from outside (through its constructor or a setter) and simply delegates:
this.strategy.calculate(this.items). Swapping pricing schemes for a cart means
handing it a different strategy object — nothing inside ShoppingCart ever changes,
and adding a new scheme means writing one new class, never touching the cart.
Example 1 — a shopping cart with interchangeable pricing
class RegularPricing {
calculate(items) {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
}
class BulkDiscountPricing {
calculate(items) {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const totalQty = items.reduce((sum, item) => sum + item.qty, 0);
return totalQty >= 10 ? total * 0.85 : total; // 15% off for bulk orders
}
}
class ClearancePricing {
calculate(items) {
const total = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return total * 0.5; // everything's 50% off
}
}
class ShoppingCart {
#items = [];
#strategy;
constructor(strategy) {
this.#strategy = strategy;
}
addItem(item) {
this.#items.push(item);
}
setStrategy(strategy) {
this.#strategy = strategy;
}
getTotal() {
return this.#strategy.calculate(this.#items);
}
}
const cart = new ShoppingCart(new RegularPricing());
cart.addItem({ price: 20, qty: 12 });
console.log(cart.getTotal()); // 240
cart.setStrategy(new BulkDiscountPricing());
console.log(cart.getTotal()); // 204 (15% off, 12 units qualifies)
cart.setStrategy(new ClearancePricing());
console.log(cart.getTotal()); // 120
ShoppingCart never asks "what kind of pricing is this" — it just calls
.calculate() on whatever strategy it's holding. Adding a
LoyaltyMemberPricing class next month requires zero edits to
ShoppingCart itself.
Example 2 — a secondary context: navigation routing
interface RouteStrategy {
String buildRoute(String from, String to);
}
class FastestRoute implements RouteStrategy {
public String buildRoute(String from, String to) {
return "Highway route from " + from + " to " + to + " (fastest, tolls included)";
}
}
class ScenicRoute implements RouteStrategy {
public String buildRoute(String from, String to) {
return "Coastal backroads from " + from + " to " + to + " (scenic, +40% time)";
}
}
class AvoidTollsRoute implements RouteStrategy {
public String buildRoute(String from, String to) {
return "Surface streets from " + from + " to " + to + " (no tolls, +15% time)";
}
}
class Navigator {
private RouteStrategy strategy;
public Navigator(RouteStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(RouteStrategy strategy) {
this.strategy = strategy;
}
public void navigate(String from, String to) {
System.out.println(strategy.buildRoute(from, to));
}
}
public class Demo {
public static void main(String[] args) {
Navigator nav = new Navigator(new FastestRoute());
nav.navigate("Home", "Office"); // Highway route...
nav.setStrategy(new ScenicRoute());
nav.navigate("Home", "Office"); // Coastal backroads...
nav.setStrategy(new AvoidTollsRoute());
nav.navigate("Home", "Office"); // Surface streets...
}
}
Same shape again: Navigator holds a RouteStrategy reference and never
contains routing logic itself — the user picks which strategy to use (fastest, scenic, avoid
tolls) from outside, and Navigator just executes whichever one it's given.
👍 When To Use It
- You have multiple interchangeable ways to do the same kind of task, and expect that list to keep growing over the object's lifetime.
- A method is picking between algorithm variants with a type flag or big conditional, and that logic keeps needing edits whenever a new variant shows up.
- You want to unit test each algorithm in complete isolation from the context that uses it.
👎 When NOT To Use It
- When there's realistically only ever going to be one algorithm — the extra interface-plus-class ceremony isn't worth it for something that will never vary.
- When the "strategies" would need to share so much state and helper logic that they end up tightly coupled to each other anyway, undermining the whole point of interchangeability.
- When the choice of algorithm is really about the object's own internal lifecycle rather than an external choice — that's a sign you actually want State, not Strategy (see the FAQ below).
❓ FAQ
How is Strategy different from State — they look identical?
Structurally they really are the same shape — an interface, interchangeable
implementations, and a context that delegates to whichever one it's holding. The difference
is in intent and where the decision comes from. Strategy is about a client
choosing which interchangeable algorithm to use — the strategy is passed in from outside by
whoever constructs or configures the context (as with ShoppingCart or
Navigator above), and it has no notion of "the next strategy" or any relationship
to the other strategies. State is about an object's behavior changing as
its own internal state evolves over time — state objects typically know about and
trigger transitions to each other, representing a point in the object's own lifecycle rather
than an outside choice. If the object itself is driving transitions between the
implementations, it's State; if a caller just hands in whichever one it wants used right now,
it's Strategy.
Isn't Strategy just a fancy way of passing a function as an argument?
In languages with first-class functions, a plain function or lambda often is a
perfectly good Strategy — you don't strictly need a class with one method. The pattern
predates first-class functions being common, so classic textbook examples use classes with a
shared interface; in JavaScript, Python, or C# today, passing cart.setStrategy((items)
=> ...) as a plain function achieves the exact same decoupling with less
boilerplate, as long as the strategy doesn't need to carry its own extra state or multiple
methods.
Where should the decision of which strategy to use actually live?
Outside the context, by design — a factory, a configuration value, or the calling code typically decides which concrete strategy to construct and hand in. The context class itself should stay ignorant of how that decision gets made; its only job is delegating to whichever strategy object it currently holds.
🙋 There Are No Dumb Questions
Q: If I only ever have two strategies, is it still worth doing this?
A: Usually not by itself — a single well-named if is often clearer for exactly two
stable options. Strategy earns its keep once you expect a third, fourth, or Nth option to show
up over time, or once each option's logic is substantial enough that you want it testable and
readable in complete isolation.
✅ Quick Check
In the Strategy pattern, who typically decides which concrete strategy a context object uses?
Next up: what happens when several classes share the exact same overall algorithm, with only a couple of steps that differ between them? That's the Template Method pattern.