Strategy pattern
Strategy Pattern in JS
The Strategy Pattern is one of the cleanest ways to remove if/else or switch hell in Node.js.
Think of it like this:
"I have multiple ways to do the same task, and I want to swap them dynamically."
What is Strategy Pattern?
It is a behavioral design pattern where:
- you define a common contract (a shape all strategies must follow)
- create multiple implementations (strategies)
- choose one strategy at runtime
Real-world analogy
Imagine a payment system:
- Credit Card payment
- UPI payment
- PayPal payment
All of them do the same thing:
pay(amount)
But the implementation differs.
Instead of doing this:
if (paymentType === 'upi') {
// UPI logic
} else if (paymentType === 'card') {
// Card logic
} else if (paymentType === 'paypal') {
// PayPal logic
}
You use Strategy Pattern.
Structure
It has 3 parts:
1. Strategy Contract
Defines the method every strategy must implement (enforced by convention or JSDoc).
2. Concrete Strategies
Actual implementations.
3. Context
The class that uses the strategy.
Class-based Example
Problem: Different discount calculation strategies
Strategy contract (via JSDoc)
/**
* @typedef {Object} DiscountStrategy
* @property {function(number): number} calculate
*/
Concrete strategies
class NoDiscountStrategy {
calculate(price) {
return price;
}
}
class FlatDiscountStrategy {
constructor(discountAmount) {
this.discountAmount = discountAmount;
}
calculate(price) {
return price - this.discountAmount;
}
}
class PercentageDiscountStrategy {
constructor(percentage) {
this.percentage = percentage;
}
calculate(price) {
return price - (price * this.percentage) / 100;
}
}
Context class
class PriceCalculator {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
getFinalPrice(price) {
return this.strategy.calculate(price);
}
}
Usage
const calculator = new PriceCalculator(new NoDiscountStrategy());
console.log(calculator.getFinalPrice(1000)); // 1000
calculator.setStrategy(new FlatDiscountStrategy(200));
console.log(calculator.getFinalPrice(1000)); // 800
calculator.setStrategy(new PercentageDiscountStrategy(10));
console.log(calculator.getFinalPrice(1000)); // 900
Functional Approach (idiomatic JS)
In JavaScript you can also express strategies as plain functions — no classes needed.
const noDiscount = (price) => price;
const flatDiscount = (amount) => (price) => price - amount;
const percentDiscount = (pct) => (price) => price - (price * pct) / 100;
function getPrice(price, strategy) {
return strategy(price);
}
getPrice(1000, noDiscount); // 1000
getPrice(1000, flatDiscount(200)); // 800
getPrice(1000, percentDiscount(10)); // 900
This is the shortest, most idiomatic JS version.
Use classes when strategies carry state or methods. Use functions when strategies are stateless transforms.
Real backend use cases in Node.js
1) Payment Gateway Selection
class RazorpayStrategy {
async pay(amount) {
console.log(`Processing ₹${amount} via Razorpay`);
}
}
class StripeStrategy {
async pay(amount) {
console.log(`Processing $${amount} via Stripe`);
}
}
class PaymentService {
constructor(strategy) {
this.strategy = strategy;
}
async checkout(amount) {
await this.strategy.pay(amount);
}
}
const service = new PaymentService(new RazorpayStrategy());
await service.checkout(500);
2) Delivery Partner Selection
class DunzoStrategy {
async createShipment(orderId) {
console.log(`Dunzo shipment created for ${orderId}`);
}
}
class ShadowfaxStrategy {
async createShipment(orderId) {
console.log(`Shadowfax shipment created for ${orderId}`);
}
}
class ShipmentService {
constructor(strategy) {
this.strategy = strategy;
}
async ship(orderId) {
await this.strategy.createShipment(orderId);
}
}
3) Carrier Integration with Strategy + Factory
A very common production pattern — the Factory picks the right strategy.
class CompanyStrategy {
async createShipment(order) {
return {
provider: 'Company',
payload: { orderId: order.id, customerName: order.customerName },
};
}
}
class PorterStrategy {
async createShipment(order) {
return {
provider: 'Porter',
payload: { shipment_id: order.id, name: order.customerName },
};
}
}
class ShadowfaxStrategy {
async createShipment(order) {
return {
provider: 'Shadowfax',
payload: { order_ref: order.id, recipient_name: order.customerName },
};
}
}
// Factory picks the strategy
const carrierStrategies = {
company: new CompanyStrategy(),
porter: new PorterStrategy(),
shadowfax: new ShadowfaxStrategy(),
};
function getCarrierStrategy(carrier) {
const strategy = carrierStrategies[carrier.toLowerCase()];
if (!strategy) throw new Error(`Unsupported carrier: ${carrier}`);
return strategy;
}
// Context
class CarrierService {
constructor(strategy) {
this.strategy = strategy;
}
async createShipment(order) {
return this.strategy.createShipment(order);
}
}
// Usage
const order = { id: 'ORD123', customerName: 'Prajwal' };
const strategy = getCarrierStrategy('porter');
const carrierService = new CarrierService(strategy);
const result = await carrierService.createShipment(order);
console.log(result);
// { provider: 'Porter', payload: { shipment_id: 'ORD123', name: 'Prajwal' } }
4) Notification Channel Strategy
class EmailStrategy {
async send(message) {
console.log('Email:', message);
}
}
class SmsStrategy {
async send(message) {
console.log('SMS:', message);
}
}
class WhatsAppStrategy {
async send(message) {
console.log('WhatsApp:', message);
}
}
class NotificationService {
constructor(strategy) {
this.strategy = strategy;
}
async notify(message) {
await this.strategy.send(message);
}
}
Benefits
1. Open/Closed Principle
Add new strategies without modifying existing code.
class FestivalDiscountStrategy {
calculate(price) {
return price * 0.7; // 30% off
}
}
No need to touch old logic.
2. Removes giant conditionals
No huge switch blocks in business logic.
3. Easy to test
// Jest
test('FlatDiscountStrategy subtracts flat amount', () => {
const strategy = new FlatDiscountStrategy(100);
expect(strategy.calculate(1000)).toBe(900);
});
Each strategy is a tiny, isolated unit.
4. Runtime flexibility
Swap behavior dynamically — from DB config, tenant settings, request params.
When to use Strategy Pattern
- you have multiple interchangeable algorithms
- behavior changes based on: provider, country, tenant, payment method, carrier, pricing logic
- you want to add new variants without touching existing code
When NOT to use it
Do not use it if:
- you only have 1 or 2 small conditions
- abstraction makes code harder to read
Bad use:
if (isAdmin) {
// ...
} else {
// ...
}
Don't create 12 classes for that. That's design-pattern cosplay.
Strategy Pattern vs Factory Pattern
Strategy Pattern — focuses on behavior
"How should this task be performed?"
Factory Pattern — focuses on object creation
"Which object should I create?"
In real apps they are used together:
const strategy = getCarrierStrategy(carrier); // Factory
await strategy.createShipment(order); // Strategy
Interview definition (short answer)
"Strategy Pattern is a behavioral design pattern that allows selecting an algorithm or behavior at runtime by encapsulating each implementation into separate classes or functions behind a common interface."
Best mental model
If you see code like:
switch (provider) {
case 'x': ...
case 'y': ...
case 'z': ...
}
Ask yourself:
"Can this become a strategy?"
A lot of backend integration code should become one.
Formula:
Contract → Multiple Strategies → Context → Optional Factory