JavaScript Strategy Pattern: Complete Guide
A complete guide to the JavaScript strategy pattern. Covers runtime algorithm swapping, validation strategies, sorting strategies, pricing engines, authentication strategies, and strategy selection with maps and registries.
The strategy pattern defines a family of interchangeable algorithms and lets you swap them at runtime without changing the code that uses them. In JavaScript, functions are first-class citizens, making strategy implementation natural and concise.
For another behavioral pattern that complements strategy, see JavaScript Observer Pattern: Complete Guide.
Core Strategy Pattern
class PaymentContext {
#strategy = null;
setStrategy(strategy) {
this.#strategy = strategy;
return this;
}
async pay(amount, details) {
if (!this.#strategy) {
throw new Error("No payment strategy set");
}
console.log(`Processing $${amount} payment...`);
const result = await this.#strategy.execute(amount, details);
console.log(`Payment ${result.status}: ${result.transactionId}`);
return result;
}
}
// Strategies as objects
const creditCardStrategy = {
name: "Credit Card",
execute(amount, { cardNumber, expiry, cvv }) {
return {
status: "completed",
transactionId: `cc_${Date.now()}`,
method: "credit_card",
amount,
last4: cardNumber.slice(-4),
};
},
};
const paypalStrategy = {
name: "PayPal",
execute(amount, { email }) {
return {
status: "completed",
transactionId: `pp_${Date.now()}`,
method: "paypal",
amount,
email,
};
},
};
const cryptoStrategy = {
name: "Crypto",
async execute(amount, { wallet, currency }) {
// Simulate blockchain confirmation
await new Promise((r) => setTimeout(r, 100));
return {
status: "pending",
transactionId: `crypto_${Date.now()}`,
method: "crypto",
amount,
wallet,
currency,
};
},
};
// Usage
const payment = new PaymentContext();
payment.setStrategy(creditCardStrategy);
await payment.pay(99.99, { cardNumber: "4242424242424242", expiry: "12/26", cvv: "123" });
payment.setStrategy(paypalStrategy);
await payment.pay(49.99, { email: "user@example.com" });Validation Strategy
function createValidator(strategies) {
return {
validate(data, ruleSets) {
const errors = {};
for (const [field, rules] of Object.entries(ruleSets)) {
const fieldErrors = [];
for (const rule of rules) {
const strategyName = typeof rule === "string" ? rule : rule.name;
const params = typeof rule === "string" ? {} : rule;
const strategy = strategies[strategyName];
if (!strategy) {
throw new Error(`Unknown validation strategy: ${strategyName}`);
}
const error = strategy(data[field], params, field, data);
if (error) fieldErrors.push(error);
}
if (fieldErrors.length > 0) {
errors[field] = fieldErrors;
}
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
},
addStrategy(name, fn) {
strategies[name] = fn;
return this;
},
};
}
// Built-in strategies
const validationStrategies = {
required: (value) =>
!value && value !== 0 ? "This field is required" : null,
minLength: (value, { min }) =>
value && value.length < min ? `Minimum ${min} characters` : null,
maxLength: (value, { max }) =>
value && value.length > max ? `Maximum ${max} characters` : null,
email: (value) =>
value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? "Invalid email address"
: null,
numeric: (value) =>
value && isNaN(Number(value)) ? "Must be a number" : null,
range: (value, { min, max }) => {
const num = Number(value);
if (num < min || num > max) return `Must be between ${min} and ${max}`;
return null;
},
pattern: (value, { regex, message }) =>
value && !new RegExp(regex).test(value) ? message || "Invalid format" : null,
matches: (value, { field: otherField }, _, data) =>
value !== data?.[otherField] ? `Must match ${otherField}` : null,
};
// Usage
const validator = createValidator(validationStrategies);
// Add custom strategy
validator.addStrategy("strongPassword", (value) => {
if (!value) return null;
if (value.length < 8) return "Password must be at least 8 characters";
if (!/[A-Z]/.test(value)) return "Must contain uppercase letter";
if (!/[0-9]/.test(value)) return "Must contain a number";
if (!/[^A-Za-z0-9]/.test(value)) return "Must contain a special character";
return null;
});
const result = validator.validate(
{ name: "A", email: "invalid", password: "weak", age: "200" },
{
name: ["required", { name: "minLength", min: 2 }],
email: ["required", "email"],
password: ["required", "strongPassword"],
age: ["numeric", { name: "range", min: 1, max: 150 }],
}
);
console.log(result);
// { valid: false, errors: { name: ["Minimum 2 characters"], email: ["Invalid email address"], password: ["Password must be at least 8 characters", ...], age: ["Must be between 1 and 150"] } }Sorting Strategy
const sortStrategies = {
alphabetical: (a, b) => a.name.localeCompare(b.name),
reverseAlphabetical: (a, b) => b.name.localeCompare(a.name),
priceAsc: (a, b) => a.price - b.price,
priceDesc: (a, b) => b.price - a.price,
newest: (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
oldest: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
popularity: (a, b) => b.views - a.views,
rating: (a, b) => b.rating - a.rating || b.reviewCount - a.reviewCount,
};
function createSortableCollection(items = []) {
let currentStrategy = "alphabetical";
return {
setStrategy(strategyName) {
if (!sortStrategies[strategyName]) {
throw new Error(
`Unknown sort: "${strategyName}". Available: ${Object.keys(sortStrategies).join(", ")}`
);
}
currentStrategy = strategyName;
return this;
},
sort() {
return [...items].sort(sortStrategies[currentStrategy]);
},
addCustomSort(name, compareFn) {
sortStrategies[name] = compareFn;
return this;
},
getSorted(strategyName) {
const strategy = sortStrategies[strategyName || currentStrategy];
return [...items].sort(strategy);
},
multiSort(...strategies) {
return [...items].sort((a, b) => {
for (const strategyName of strategies) {
const result = sortStrategies[strategyName](a, b);
if (result !== 0) return result;
}
return 0;
});
},
getAvailableStrategies() {
return Object.keys(sortStrategies);
},
};
}
// Usage
const products = [
{ name: "Widget", price: 25, rating: 4.5, views: 1200, reviewCount: 89, createdAt: "2025-01-15" },
{ name: "Gadget", price: 49, rating: 4.8, views: 800, reviewCount: 45, createdAt: "2025-03-20" },
{ name: "Doohickey", price: 15, rating: 4.2, views: 2000, reviewCount: 150, createdAt: "2024-11-01" },
];
const collection = createSortableCollection(products);
console.log(collection.setStrategy("priceAsc").sort());
console.log(collection.getSorted("rating"));
console.log(collection.multiSort("rating", "priceAsc"));Pricing Engine
class PricingEngine {
#strategies = new Map();
#defaultStrategy = null;
register(name, strategy) {
this.#strategies.set(name, strategy);
return this;
}
setDefault(name) {
this.#defaultStrategy = name;
return this;
}
calculate(items, strategyName, context = {}) {
const name = strategyName || this.#defaultStrategy;
const strategy = this.#strategies.get(name);
if (!strategy) throw new Error(`Unknown pricing strategy: ${name}`);
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
return strategy(items, subtotal, context);
}
}
const pricing = new PricingEngine();
pricing
.register("standard", (items, subtotal) => ({
subtotal,
discount: 0,
tax: subtotal * 0.08,
total: subtotal * 1.08,
breakdown: "Standard pricing",
}))
.register("member", (items, subtotal, { memberTier }) => {
const discountRates = { silver: 0.05, gold: 0.10, platinum: 0.15 };
const rate = discountRates[memberTier] || 0;
const discount = subtotal * rate;
const afterDiscount = subtotal - discount;
return {
subtotal,
discount,
tax: afterDiscount * 0.08,
total: afterDiscount * 1.08,
breakdown: `${memberTier} member: ${rate * 100}% off`,
};
})
.register("bulk", (items, subtotal) => {
const totalQty = items.reduce((sum, i) => sum + i.qty, 0);
let discountRate = 0;
if (totalQty >= 100) discountRate = 0.20;
else if (totalQty >= 50) discountRate = 0.15;
else if (totalQty >= 20) discountRate = 0.10;
const discount = subtotal * discountRate;
const afterDiscount = subtotal - discount;
return {
subtotal,
discount,
tax: afterDiscount * 0.08,
total: afterDiscount * 1.08,
breakdown: `Bulk: ${discountRate * 100}% off for ${totalQty} units`,
};
})
.register("promotional", (items, subtotal, { promoCode }) => {
const promos = { SAVE20: 0.20, SUMMER10: 0.10, WELCOME: 0.15 };
const rate = promos[promoCode] || 0;
const discount = subtotal * rate;
const afterDiscount = subtotal - discount;
return {
subtotal,
discount,
tax: afterDiscount * 0.08,
total: afterDiscount * 1.08,
breakdown: rate > 0
? `Promo "${promoCode}": ${rate * 100}% off`
: `Invalid promo: "${promoCode}"`,
};
})
.setDefault("standard");
// Usage
const cart = [
{ name: "Widget", price: 29.99, qty: 3 },
{ name: "Gadget", price: 49.99, qty: 1 },
];
console.log(pricing.calculate(cart, "standard"));
console.log(pricing.calculate(cart, "member", { memberTier: "gold" }));
console.log(pricing.calculate(cart, "promotional", { promoCode: "SAVE20" }));Authentication Strategy
function createAuthSystem() {
const strategies = new Map();
let currentStrategy = null;
return {
register(name, strategy) {
strategies.set(name, strategy);
return this;
},
use(name) {
if (!strategies.has(name)) {
throw new Error(`Auth strategy "${name}" not registered`);
}
currentStrategy = name;
return this;
},
async authenticate(credentials) {
if (!currentStrategy) throw new Error("No auth strategy selected");
const strategy = strategies.get(currentStrategy);
try {
const user = await strategy.verify(credentials);
return { success: true, user, method: currentStrategy };
} catch (error) {
return { success: false, error: error.message, method: currentStrategy };
}
},
async tryAll(credentials) {
for (const [name, strategy] of strategies) {
try {
const user = await strategy.verify(credentials);
return { success: true, user, method: name };
} catch {
continue;
}
}
return { success: false, error: "All strategies failed" };
},
};
}
const auth = createAuthSystem();
auth
.register("local", {
async verify({ username, password }) {
// Simulate DB lookup
if (username === "admin" && password === "secret") {
return { id: "1", username: "admin", role: "admin" };
}
throw new Error("Invalid credentials");
},
})
.register("apiKey", {
async verify({ apiKey }) {
const validKeys = { abc123: { id: "2", name: "API User", role: "api" } };
const user = validKeys[apiKey];
if (!user) throw new Error("Invalid API key");
return user;
},
})
.register("jwt", {
async verify({ token }) {
// Simulate JWT decode/verify
if (!token || token.split(".").length !== 3) {
throw new Error("Invalid JWT");
}
return { id: "3", decoded: true, role: "user" };
},
});
// Use specific strategy
auth.use("local");
const result = await auth.authenticate({ username: "admin", password: "secret" });
console.log(result);| Strategy Domain | Strategies Count | Selection Method | Runtime Swap |
|---|---|---|---|
| Payment processing | 3-5 | User selection | Per transaction |
| Validation | 10-20 | Rule configuration | Per field |
| Sorting | 5-10 | UI dropdown | Per interaction |
| Pricing | 3-6 | Business rules | Per order |
| Authentication | 2-4 | Request context | Per request |
Rune AI
Key Insights
- Strategies encapsulate interchangeable algorithms behind a common interface: The context object delegates to whichever strategy is currently set, without knowing the implementation
- Function-based strategies leverage JavaScript's first-class functions: Pass algorithm functions directly instead of creating class hierarchies common in classical OOP
- Strategy registries enable runtime selection by name or configuration: Store strategies in a Map and select by key, making the choice data-driven rather than code-driven
- Validation strategies compose small, reusable rules into complex checks: Each rule is independent and testable, combined per-field through configuration arrays
- The strategy pattern eliminates growing switch/if-else chains: Adding a new algorithm means registering a new strategy, not modifying existing conditional logic
Frequently Asked Questions
How is the strategy pattern different from just passing a callback?
Can strategies have shared state?
How do I test the strategy pattern?
When should I use the strategy pattern vs if/else chains?
Conclusion
The strategy pattern lets you swap algorithms at runtime without modifying calling code. Validation strategies compose reusable rules. Sorting strategies provide user-selectable orderings. Pricing engines calculate totals with business-rule-driven strategies. Authentication strategies handle multiple login methods. For the factory pattern that creates strategy objects, see The JavaScript Factory Pattern: Complete Guide. For encapsulating strategies in modules, review Implementing the Revealing Module Pattern in JS.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.