Strategy Pattern
What is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that lets you define a family of algorithms, encapsulate each one as an object, and make them interchangeable. The client code picks which strategy to use at runtime — without knowing anything about the concrete implementation.
The classic problem this solves: a class that needs to behave differently depending on some condition, but the behavior itself keeps changing or growing. Without the pattern, you end up with sprawling if/else chains that get harder to maintain with every new algorithm you add.
With the Strategy Pattern, you extract each algorithm into its own object. The context object holds a reference to whatever strategy is currently active and delegates the work to it. Adding a new algorithm means creating a new strategy object — no touching the context.
Key Components
Context holds a reference to the active strategy and delegates work to it. It doesn’t know or care which concrete algorithm is running.
Strategy Interface defines what every concrete strategy must implement — typically a single method (or set of methods) that the context calls. In JavaScript, you don’t need a formal interface; duck typing handles this for you. Any object with the expected method works as a strategy.
Concrete Strategies are the individual algorithm implementations. They all satisfy the same interface so the context treats them uniformly.
Classic OOP Implementation
Here’s a payment processing example — one of the most common use cases:
// Strategy Interface (implicit via duck typing)
class PaymentStrategy {
pay(amount) {
throw new Error('Method pay() must be implemented');
}
}
// Concrete Strategies
class CreditCardPayment extends PaymentStrategy {
constructor(cardNumber, cvv) {
super();
this.cardNumber = cardNumber;
this.cvv = cvv;
}
pay(amount) {
return `Paid ${amount} using Credit Card ${this.cardNumber.slice(-4)}`;
}
}
class PayPalPayment extends PaymentStrategy {
constructor(email) {
super();
this.email = email;
}
pay(amount) {
return `Paid ${amount} using PayPal account ${this.email}`;
}
}
class BankTransferPayment extends PaymentStrategy {
constructor(accountNumber, routingNumber) {
super();
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
}
pay(amount) {
return `Paid ${amount} via bank transfer ${this.accountNumber.slice(-4)}`;
}
}
// Context
class ShoppingCart {
constructor() {
this.items = [];
this.paymentStrategy = null;
}
addItem(item, price) {
this.items.push({ item, price });
}
setPaymentStrategy(strategy) {
if (!(strategy instanceof PaymentStrategy)) {
throw new Error('Strategy must be a PaymentStrategy instance');
}
this.paymentStrategy = strategy;
}
checkout() {
const total = this.items.reduce((sum, i) => sum + i.price, 0);
if (!this.paymentStrategy) {
throw new Error('No payment strategy set');
}
return this.paymentStrategy.pay(total);
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem('Keyboard', 149);
cart.addItem('Mouse', 59);
cart.setPaymentStrategy(new CreditCardPayment('4111111111111111', '123'));
console.log(cart.checkout());
// Paid 208 using Credit Card 1111
cart.setPaymentStrategy(new PayPalPayment('alex@example.com'));
console.log(cart.checkout());
// Paid 208 using PayPal account alex@example.com
cart.setPaymentStrategy(new BankTransferPayment('1234567890', '021000021'));
console.log(cart.checkout());
// Paid 208 via bank transfer 7890
```javascript
This keeps the cart simple — it only knows about the strategy interface, not the details of any payment method.
## Functional Strategy Pattern
When each strategy only needs a single method, you can skip classes entirely and use plain objects or functions. This is idiomatic JavaScript:
```javascript
// Strategies as plain functions
const deliveryStrategies = {
rush: (address) => `Rush → ${address} (1-2 days)`,
standard: (address) => `Standard → ${address} (5-7 days)`,
economy: (address) => `Economy → ${address} (10-14 days)`,
};
// Context factory
function createShippingCalculator(strategyFn) {
return {
calculate(address) {
if (typeof strategyFn !== 'function') {
throw new Error('Invalid strategy');
}
return strategyFn(address);
},
setStrategy(newStrategy) {
strategyFn = newStrategy;
}
};
}
// Usage
const shipping = createShippingCalculator(deliveryStrategies.standard);
console.log(shipping.calculate('742 Evergreen Terrace'));
// Standard → 742 Evergreen Terrace (5-7 days)
shipping.setStrategy(deliveryStrategies.rush);
console.log(shipping.calculate('742 Evergreen Terrace'));
// Rush → 742 Evergreen Terrace (1-2 days)
```javascript
This approach works well when your algorithms are simple enough to express as pure functions. No class inheritance, no ceremony.
## Real-World Example: Form Validation
Validation is a natural fit for the Strategy Pattern because you often need different rules for different fields, and those rules change independently:
```javascript
// Strategy factories — functions that return validators
const validators = {
required: () => (value) => {
const isValid = value !== null && value !== undefined && value.trim().length > 0;
return isValid ? null : 'This field is required';
},
email: () => (value) => {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(value) ? null : 'Enter a valid email address';
},
minLength: (min) => (value) => {
return value.length >= min ? null : `Must be at least ${min} characters`;
},
maxLength: (max) => (value) => {
return value.length <= max ? null : `Must be no more than ${max} characters`;
},
pattern: (regex, message) => (value) => {
return regex.test(value) ? null : message;
}
};
// Context
function createFormField(initialValue, ...fieldValidators) {
return {
value: initialValue,
validate() {
const errors = fieldValidators
.map((validator) => validator(initialValue))
.filter(Boolean);
return {
isValid: errors.length === 0,
errors
};
}
};
}
// Usage
const emailField = createFormField(
'not-an-email',
validators.required(),
validators.email()
);
const result = emailField.validate();
console.log(result.isValid); // false
console.log(result.errors); // ['Enter a valid email address']
const passwordField = createFormField(
'sh0rt',
validators.required(),
validators.minLength(8),
validators.pattern(/[A-Z]/, 'Must contain an uppercase letter'),
validators.pattern(/[0-9]/, 'Must contain a number')
);
const pwResult = passwordField.validate();
console.log(pwResult.isValid); // false
console.log(pwResult.errors);
// ['Must be at least 8 characters', 'Must contain an uppercase letter']
```javascript
Each validator is a strategy. The form field doesn't know or care how validation works — it just runs whatever strategies you give it. You can combine validators freely, and adding a new one doesn't require touching the field implementation.
## Common Mistakes and Gotchas
**Strategy explosion.** If you find yourself creating dozens of tiny strategy classes that each do almost nothing, you've gone too far. Group related strategies. If a strategy needs configuration, use a factory function to create parameterized instances.
**Shared mutable state.** Strategies should be stateless or own their state entirely. If two strategies share a reference to a mutable object, they'll interfere with each other in ways that are hard to debug. Pass all required data through the strategy's method parameters.
**Overusing the pattern.** If you have two behaviors that will never grow, a simple conditional is clearer than the full pattern. The Strategy Pattern pays off when behaviors vary and change independently over time. If the logic is stable and likely to stay that way, skip the indirection.
**Missing strategy handling.** Always decide what happens when no strategy is set. Throwing an error (as shown above) is better than silent failure or returning undefined. For cases where a default makes sense, set it in the context's constructor.
**Context knows too much.** The context should only interact with the strategy through its defined interface. If you find the context accessing strategy properties directly, the boundary has leaked.
## How Strategy Differs from State
People often confuse Strategy with [State](/tutorials/javascript-design-patterns/dp-state-machines/), but they're solving different problems.
State objects encapsulate state-specific behavior and the context often transitions between states itself. The context doesn't just delegate — it often manages which state is active and when to switch.
Strategy objects encapsulate algorithms. The context delegates to the strategy and that's it — it doesn't manage strategy transitions. The client code typically decides which strategy to use and when.
Think of it this way: Strategy is about swapping the algorithm the context runs. State is about modeling the context as being in one of several states, where each state influences behavior.
## When to Use
The Strategy Pattern makes sense when:
- You have multiple ways to do the same thing, and you need to pick one at runtime
- You add or change algorithms more often than you change the context itself
- You want to avoid massive switch statements or if/else chains
- Different algorithm variants need different configurations
- You want to test each algorithm in isolation
The payment example is classic, but you'll also see it in sorting (choosing QuickSort vs MergeSort based on data characteristics), routing (fastest vs shortest vs scenic routes), compression (ZIP vs RAR vs tar), and authentication (API key vs OAuth vs JWT strategies).
## See Also
- [Command Pattern](/tutorials/javascript-design-patterns/dp-command-pattern/) — encapsulate requests as objects, often paired with Strategy in undo/redo systems
- [Observer Pattern](/tutorials/javascript-design-patterns/dp-observer-pattern/) — decoupled communication between objects
- [State Machines](/tutorials/javascript-design-patterns/dp-state-machines/) — model an entity that transitions between discrete states