Behavioral Pattern Pattern Deep Dive

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

Primary Example — JavaScript
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

Secondary Example — Java
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.

← Back to
Next →