JavaScript Strategy Pattern: Complete Guide
The strategy pattern lets you swap algorithms at runtime by encapsulating each one in a separate object or function. Learn how to use it to write flexible, maintainable JavaScript code.
The strategy pattern lets you change how a piece of code behaves by swapping out the algorithm it uses, at runtime, without changing the code that uses it. You define a family of interchangeable algorithms, encapsulate each one, and let the caller choose which one to plug in.
Think of a navigation app. You enter a destination. The app can calculate a route by "fastest time," "shortest distance," or "avoid highways." The core navigation logic stays the same. Only the routing algorithm changes. Each routing option is a strategy.
The Problem: Growing Conditional Chains
Here is code that calculates shipping costs without the strategy pattern:
function calculateShipping(method, weight) {
if (method === "standard") {
return weight * 1.5 + 5;
} else if (method === "express") {
return weight * 3.0 + 10;
} else if (method === "overnight") {
return weight * 5.0 + 20;
} else if (method === "international") {
return weight * 8.0 + 35;
}
throw new Error(`Unknown shipping method: ${method}`);
}
console.log(calculateShipping("express", 2)); // 16Every new shipping method means adding another else if branch. The function grows. Testing gets harder. The code that uses calculateShipping must know about all the method strings.
The Solution: Encapsulate Each Strategy
Turn each shipping method into its own function:
const shippingStrategies = {
standard(weight) {
return weight * 1.5 + 5;
},
express(weight) {
return weight * 3.0 + 10;
},
overnight(weight) {
return weight * 5.0 + 20;
},
international(weight) {
return weight * 8.0 + 35;
}
};
function calculateShipping(method, weight) {
const strategy = shippingStrategies[method];
if (!strategy) {
throw new Error(`Unknown shipping method: ${method}`);
}
return strategy(weight);
}
console.log(calculateShipping("express", 2)); // 16The context function calculateShipping now delegates to a strategy object. Adding a new shipping method is one line in the shippingStrategies object. The context function never changes.
The context only knows that it has a method name and a weight. It looks up the matching strategy and delegates. Each strategy is a self-contained function that can be tested in isolation.
Passing Strategies as Arguments
Strategies do not need to live in a lookup object. You can pass them directly:
function processPayment(amount, paymentStrategy) {
console.log(`Processing $${amount}...`);
const result = paymentStrategy(amount);
console.log(result);
}
const creditCard = (amount) => `Charged $${amount} to credit card`;
const paypal = (amount) => `Sent $${amount} via PayPal`;
const crypto = (amount) => `Transferred $${amount} in USDC`;
processPayment(99, creditCard);
// Processing $99...
// Charged $99 to credit card
processPayment(50, paypal);
// Processing $50...
// Sent $50 via PayPalThis is the most common form of the strategy pattern in JavaScript. Higher-order functions, callback functions, and event handlers are all examples of passing a strategy as an argument.
Strategy as a Class-Like Object
When a strategy needs its own state or configuration, use an object with a standard method name:
class DiscountStrategy {
calculate(price) {
return price; // No discount by default
}
}
class PercentageDiscount extends DiscountStrategy {
constructor(percent) {
super();
this.percent = percent;
}
calculate(price) {
return price * (1 - this.percent / 100);
}
}
class FixedDiscount extends DiscountStrategy {
constructor(amount) {
super();
this.amount = amount;
}
calculate(price) {
return Math.max(0, price - this.amount);
}
}
function getFinalPrice(price, discountStrategy) {
return discountStrategy.calculate(price);
}
const tenPercent = new PercentageDiscount(10);
const fiveDollarsOff = new FixedDiscount(5);
console.log(getFinalPrice(100, tenPercent)); // 90
console.log(getFinalPrice(100, fiveDollarsOff)); // 95Every strategy has a calculate method. The context getFinalPrice only calls discountStrategy.calculate(price). It does not know or care whether the discount is percentage-based or fixed.
For more on using classes this way, see the JavaScript classes guide.
Real-World Use Case: Formatter Strategies
A practical example: a data display component that formats values differently depending on context:
const formatters = {
currency(value) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(value);
},
percent(value) {
return `${(value * 100).toFixed(1)}%`;
},
compact(value) {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
return String(value);
},
default(value) {
return String(value);
}
};
function renderCell(value, format) {
const formatter = formatters[format] || formatters.default;
return formatter(value);
}
console.log(renderCell(1234.56, "currency")); // $1,234.56
console.log(renderCell(0.873, "percent")); // 87.3%
console.log(renderCell(45000, "compact")); // 45.0KNo if or switch in renderCell. Every format is its own function. Adding a new format is one entry in the formatters object.
Strategy Pattern vs If-Else: When to Switch
| Use if-else/switch when | Use strategy pattern when |
|---|---|
| Two or three branches | Four or more branches |
| Branches will not grow | Branches are likely to grow |
| Logic is simple one-liners | Each branch has nontrivial logic |
| All branches in one file | Branches may come from different modules |
The inflection point is typically three branches. Below that, conditionals are clearer. Above that, the strategy pattern pays for its setup cost.
Common Mistake: Over-Engineering
Not every conditional needs to become a strategy:
// Over-engineered for a simple case
const greetingStrategies = {
morning: () => "Good morning",
afternoon: () => "Good afternoon",
evening: () => "Good evening"
};
// A simple object lookup is clearer here
const greetings = {
morning: "Good morning",
afternoon: "Good afternoon",
evening: "Good evening"
};The strategy pattern is about encapsulating behavior (functions), not data (strings). If your "strategies" return static values, use a plain object lookup instead.
Rune AI
Key Insights
- The strategy pattern encapsulates interchangeable algorithms behind a common interface.
- It replaces long if-else or switch chains with composable behavior objects.
- In JavaScript, strategies are often just functions passed as arguments.
- The context delegates work to the strategy without knowing its internals.
- Adding a new strategy means writing one new function, not modifying existing code.
Frequently Asked Questions
What is the difference between the strategy pattern and the factory pattern?
Can I use plain functions instead of classes for strategies?
When should I avoid the strategy pattern?
Conclusion
The strategy pattern is about choosing behaviors at runtime instead of hard-coding them with conditionals. Encapsulate each algorithm in a function or object, pass the one you want to the context, and let the context delegate to it. The result is code that is easier to extend and test.The strategy pattern replaces conditional chains with interchangeable behavior objects. Define each algorithm as a separate function. Let the context accept a strategy and delegate to it. Add new behaviors by writing new functions, not by editing existing if-else chains.
In JavaScript, the pattern is often so natural you do not even call it by name. Passing a callback to Array.sort(), a comparison function to Array.filter(), or a reducer to Array.reduce() are all strategy pattern applications. Recognizing the pattern helps you use it intentionally when your own conditional chains start to grow.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.