Strategy: Pulling Algorithms Out So Behavior Stays Swappable
Starting from a sprawling if-else discount calculator, see how the Strategy Pattern packages each algorithm into an interchangeable strategy object — and where the line falls between it and the State Pattern.
What It Looks Like Without the Strategy Pattern
Say you're writing the discount logic for a shopping cart. The most obvious approach is usually a wall of if-else:
function calculateDiscount(type, amount) {
if (type === 'regular') {
return amount
} else if (type === 'vip') {
return amount * 0.8
} else if (type === 'holiday') {
return amount > 1000 ? amount - 200 : amount
} else if (type === 'newUser') {
return amount > 500 ? amount * 0.9 : amount
}
return amount
}The problem is that every new discount rule means coming back to add another else if. This function only ever grows longer and uglier — and that same branching logic is likely duplicated elsewhere too, on the checkout page, the cart page, wherever else the discount gets calculated. Miss updating one of them when the rules change, and you've got inconsistent behavior.
Splitting Each Algorithm Out With the Strategy Pattern
The Strategy Pattern's approach is to wrap each algorithm in its own object, all exposing the same interface, and hand the decision of which one to use at runtime over to a Context object — instead of hardcoding the branching logic into one giant function.
class RegularDiscount {
calculate(amount) {
return amount
}
}
class VipDiscount {
calculate(amount) {
return amount * 0.8
}
}
class HolidayDiscount {
calculate(amount) {
return amount > 1000 ? amount - 200 : amount
}
}
class NewUserDiscount {
calculate(amount) {
return amount > 500 ? amount * 0.9 : amount
}
}Then define a Context that holds the currently active strategy and delegates the actual calculation to it:
class PriceCalculator {
#strategy
constructor(strategy) {
this.#strategy = strategy
}
setStrategy(strategy) {
this.#strategy = strategy
}
calculate(amount) {
return this.#strategy.calculate(amount)
}
}Here's how a caller uses it:
const calculator = new PriceCalculator(new VipDiscount())
console.log(calculator.calculate(1200)) // 960
calculator.setStrategy(new HolidayDiscount())
console.log(calculator.calculate(1200)) // 1000Adding a new discount rule — say, a student discount — just means writing one more class that implements the same interface. PriceCalculator doesn't change, and none of the existing discount logic gets touched. This is the Open-Closed Principle in action: open for extension, closed for modification.
In JavaScript, a Strategy Doesn't Have to Be a Class
Because functions are first-class citizens in JavaScript, you often don't need a class per strategy at all — a plain function passed in as the strategy works just as well:
const discountStrategies = {
regular: (amount) => amount,
vip: (amount) => amount * 0.8,
holiday: (amount) => (amount > 1000 ? amount - 200 : amount),
newUser: (amount) => (amount > 500 ? amount * 0.9 : amount),
}
function calculate(type, amount) {
const strategy = discountStrategies[type] ?? discountStrategies.regular
return strategy(amount)
}
console.log(calculate('vip', 1200)) // 960This is the Strategy Pattern in every way that matters — discountStrategies is a set of interchangeable strategies that all implement the same interface (take an amount, return the discounted amount), just without the ceremony of wrapping each one in a class. That's why, in JavaScript and TypeScript, the Strategy Pattern rarely looks like the UML diagram from the textbook — it looks more like a lookup table paired with a handful of functions.
Why a Shared Interface Is Non-Negotiable: That's What Makes It a Strategy
The Strategy Pattern only works because every strategy exposes the same interface — here, "take an amount, return the discounted amount" — which is exactly what lets the Context swap strategies without knowing anything about their internals:
function checkout(calculator, amount) {
const finalAmount = calculator.calculate(amount)
console.log(`Total due: $${finalAmount}`)
}
checkout(new PriceCalculator(new RegularDiscount()), 1200) // Total due: $1200
checkout(new PriceCalculator(new NewUserDiscount()), 1200) // Total due: $1080checkout never needs to know which discount strategy got passed in — and that's exactly why the Strategy Pattern is often described as making algorithms interchangeable.
When to Use It, and When It's Overengineering
The Strategy Pattern earns its keep when:
- The same task has multiple algorithms or rules, and new ones are likely to keep showing up — discount rules, sorting logic, validation rules
- You want to get rid of a sprawling if-else or switch, especially one that's duplicated across several places and has to be updated everywhere every time a rule changes
- You need to swap algorithms at runtime rather than locking them in at compile time
But if the set of strategies is fixed and unlikely to change anytime soon, a plain if-else or switch is more straightforward — forcing the Strategy Pattern on it just adds an extra layer of indirection with no real payoff. As shown earlier, in JavaScript a lookup table (an object or a Map) is often all you need — there's no requirement to actually spin up a pile of classes.
Wrapping Up
The core value of the Strategy Pattern is separating "which algorithm to use" from "how that algorithm is implemented," so each algorithm stays independent and interchangeable, and the caller never has to write a long chain of conditionals. It's structurally similar to the State Pattern, but the intent differs: Strategy is about an external caller picking between interchangeable algorithms, while State is about an object's own transitions between internal states.