Strategy Pattern
Replace If-Else Chains with Pluggable Algorithms
LinkedIn Hook
Here is the code smell that appears in almost every codebase after six months of feature requests:
A
processPayment()function with an if-else chain 80 lines long. Credit card here. PayPal there. Crypto at the bottom. A new payment method added every sprint.Every time a new method is added, a developer opens that same function, reads the whole thing, adds another
else if, and hopes nothing breaks.That's not a payment module. That's a liability.
The Strategy Pattern solves this. You define each algorithm as its own object, swap them at runtime, and never touch existing code to add a new one. The if-else chain disappears. The function shrinks to three lines.
This lesson shows the before, the after, and exactly how to explain it in an interview.
Read the full lesson with code and cheat sheet → [link]
#OOP #JavaScript #DesignPatterns #SoftwareEngineering #InterviewPrep
What You'll Learn
- What the Strategy Pattern is and the specific problem it solves in growing codebases
- How to refactor an if-else chain into interchangeable strategy objects, step by step
- A complete payment processing example: before (broken) and after (clean)
- A sorting strategy example showing how the pattern applies beyond payment systems
- The direct connection between the Strategy Pattern and the Open/Closed Principle
- How to answer the top interview questions on this pattern with precision
The Analogy That Makes It Click
Think about a GPS navigation app. When you enter a destination, it doesn't run one fixed routing algorithm. It asks you how you want to travel: driving, walking, cycling, or public transit. You pick a mode, and the app uses that specific routing strategy. Switch modes, and a completely different algorithm runs, on the same map, with the same start and end points.
The app itself doesn't change. The underlying route calculation logic is swapped out.
That's the Strategy Pattern. A context object (the GPS app) holds a reference to a strategy object (the routing algorithm). You can swap that strategy at runtime without modifying the context. Each strategy is a self-contained, interchangeable unit that follows the same interface.
The key insight is this: the context doesn't know or care which strategy it's using. It just calls calculateRoute() and trusts the strategy to handle the details. The calling code stays stable. The algorithms are free to vary.
[INTERNAL-LINK: Open/Closed Principle foundation → Lesson 7.2: Open/Closed Principle]
What Is the Strategy Pattern?
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable at runtime through a shared interface. The object that uses the algorithm (the context) holds a reference to the current strategy and delegates work to it.
The pattern has three parts. The strategy interface declares the method that all concrete strategies must implement. The concrete strategy classes each implement that interface with their own algorithm. The context class holds a reference to the current strategy and calls it when needed.
This structure keeps the context class small and stable. New algorithms are added as new classes, not as new branches in existing code. The pattern is one of the most direct implementations of the Open/Closed Principle from SOLID, which states that code should be open for extension but closed for modification.
[INTERNAL-LINK: how interfaces enforce contracts in JavaScript → Lesson 3.3: Interface]
Before: The If-Else Chain Problem
What the Code Looks Like Before the Pattern
This is the code that triggers the Strategy Pattern. A single checkout() function that handles every payment method internally. Every new payment method requires opening this function and adding another branch.
// BEFORE: if-else chain — adding any new payment method means editing this function
class PaymentProcessor {
constructor(paymentType) {
this.paymentType = paymentType;
}
checkout(amount) {
// Each branch handles a completely different algorithm
// All of them live inside one function that keeps growing
if (this.paymentType === "credit-card") {
console.log(`Processing credit card payment of $${amount}`);
console.log("Validating card number...");
console.log("Charging card via Stripe API...");
console.log(`Credit card payment of $${amount} approved.`);
} else if (this.paymentType === "paypal") {
console.log(`Processing PayPal payment of $${amount}`);
console.log("Redirecting to PayPal login...");
console.log("Confirming PayPal authorization...");
console.log(`PayPal payment of $${amount} completed.`);
} else if (this.paymentType === "crypto") {
console.log(`Processing crypto payment of $${amount}`);
console.log("Generating wallet address...");
console.log("Waiting for blockchain confirmation...");
console.log(`Crypto payment of $${amount} confirmed on chain.`);
} else if (this.paymentType === "bank-transfer") {
// Someone added this last month — buried at the bottom
console.log(`Processing bank transfer of $${amount}`);
console.log("Verifying account and routing number...");
console.log(`Bank transfer of $${amount} initiated.`);
} else {
throw new Error(`Unsupported payment type: ${this.paymentType}`);
}
}
}
const processor = new PaymentProcessor("credit-card");
processor.checkout(150);
// Output:
// Processing credit card payment of $150
// Validating card number...
// Charging card via Stripe API...
// Credit card payment of $150 approved.
const processor2 = new PaymentProcessor("paypal");
processor2.checkout(75);
// Output:
// Processing PayPal payment of $75
// Redirecting to PayPal login...
// Confirming PayPal authorization...
// PayPal payment of $75 completed.
[PERSONAL EXPERIENCE]: This pattern grows one else if at a time. The first version had two payment types and seemed harmless. By the time the team needed a fifth type, every developer was afraid to touch the function. Tests started failing in unrelated branches after unrelated edits. The function had become the single most fragile piece in the codebase.
Why This Design Fails
The function violates the Open/Closed Principle directly. Every new payment method requires modifying existing, tested code. The more branches added, the harder it becomes to test any single algorithm in isolation. A bug in the crypto logic can cause a developer to accidentally break the credit card path while fixing it. The class cannot be easily extended without risk of regression.
It also violates the Single Responsibility Principle. One class now knows how to process credit cards, handle PayPal flows, manage blockchain confirmations, and verify bank routing numbers. That's four separate responsibilities packed into a single checkout() method.
After: The Strategy Pattern Refactor
How the Pattern Restructures the Code
// AFTER: Strategy Pattern — each payment algorithm is its own class
// Step 1: Define the strategy interface (documented contract)
// In JavaScript there is no formal interface keyword, so we document the expected shape
// All concrete strategies must implement: pay(amount)
// Step 2: Concrete strategy — credit card
class CreditCardStrategy {
constructor(cardNumber, expiryDate, cvv) {
this.cardNumber = cardNumber;
this.expiryDate = expiryDate;
this.cvv = cvv;
}
pay(amount) {
console.log(`Processing credit card payment of $${amount}`);
console.log(`Validating card ending in ${this.cardNumber.slice(-4)}...`);
console.log("Charging card via Stripe API...");
console.log(`Credit card payment of $${amount} approved.`);
}
}
// Step 3: Concrete strategy — PayPal
class PayPalStrategy {
constructor(email) {
this.email = email;
}
pay(amount) {
console.log(`Processing PayPal payment of $${amount}`);
console.log(`Redirecting ${this.email} to PayPal login...`);
console.log("Confirming PayPal authorization...");
console.log(`PayPal payment of $${amount} completed.`);
}
}
// Step 4: Concrete strategy — crypto
class CryptoStrategy {
constructor(walletAddress, coinType) {
this.walletAddress = walletAddress;
this.coinType = coinType;
}
pay(amount) {
console.log(`Processing ${this.coinType} payment of $${amount}`);
console.log(`Sending to wallet: ${this.walletAddress}`);
console.log("Waiting for blockchain confirmation...");
console.log(`Crypto payment of $${amount} confirmed on chain.`);
}
}
// Step 5: The context — holds a reference to the current strategy
// It does not know or care which strategy it's holding
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy; // inject the strategy at construction time
}
// Swap the strategy at runtime — no other code changes needed
setStrategy(strategy) {
this.strategy = strategy;
}
// Delegate to the strategy — context logic stays tiny and stable
checkout(amount) {
if (!this.strategy) {
throw new Error("No payment strategy set.");
}
this.strategy.pay(amount);
}
}
// Usage: inject the strategy from the outside — the processor never changes
const creditCard = new CreditCardStrategy("4111111111111234", "12/27", "123");
const processor = new PaymentProcessor(creditCard);
processor.checkout(150);
// Output:
// Processing credit card payment of $150
// Validating card ending in 1234...
// Charging card via Stripe API...
// Credit card payment of $150 approved.
// Switch strategy at runtime — no new branches, no edits to PaymentProcessor
processor.setStrategy(new PayPalStrategy("alice@example.com"));
processor.checkout(75);
// Output:
// Processing PayPal payment of $75
// Redirecting alice@example.com to PayPal login...
// Confirming PayPal authorization...
// PayPal payment of $75 completed.
// Adding crypto support: write one new class, touch nothing else
processor.setStrategy(new CryptoStrategy("0xABCD1234", "ETH"));
processor.checkout(200);
// Output:
// Processing ETH payment of $200
// Sending to wallet: 0xABCD1234
// Waiting for blockchain confirmation...
// Crypto payment of $200 confirmed on chain.
[UNIQUE INSIGHT]: Notice that PaymentProcessor.checkout() is now three lines. That's the signal that the pattern is working correctly. The context class should be almost embarrassingly simple after the refactor. If the context still has conditional logic, the refactor is incomplete.
Adding a New Payment Method — Zero Existing Code Changed
This is the proof that the pattern works. Adding bank transfer support requires writing one new class. PaymentProcessor is never opened or modified.
// Adding a new payment method: write a new class, change nothing else
class BankTransferStrategy {
constructor(accountNumber, routingNumber) {
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
}
pay(amount) {
console.log(`Processing bank transfer of $${amount}`);
console.log(`Account: ***${this.accountNumber.slice(-4)}, Routing: ${this.routingNumber}`);
console.log("Verifying account details with bank...");
console.log(`Bank transfer of $${amount} initiated.`);
}
}
// PaymentProcessor never changed — this is OCP in action
processor.setStrategy(new BankTransferStrategy("987654321", "021000021"));
processor.checkout(500);
// Output:
// Processing bank transfer of $500
// Account: ***4321, Routing: 021000021
// Verifying account details with bank...
// Bank transfer of $500 initiated.
The Open/Closed Principle is satisfied precisely because PaymentProcessor was never reopened. It's closed for modification. The system was open for extension because a new class slot into the existing structure with no friction.
[INTERNAL-LINK: Open/Closed Principle applied to class design → Lesson 7.2: Open/Closed Principle]
Second Example: Sorting Strategies
The Strategy Pattern applies far beyond payment systems. Any situation where multiple algorithms solve the same problem and the choice should be configurable at runtime is a candidate. Sorting is a textbook case.
// Sorting strategy interface contract: sort(array) returns sorted array
class BubbleSortStrategy {
sort(data) {
const arr = [...data]; // copy to avoid mutating the original
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
for (let j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // swap
}
}
}
return arr;
}
}
class QuickSortStrategy {
sort(data) {
const arr = [...data];
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = arr.filter(x => x < pivot);
const middle = arr.filter(x => x === pivot);
const right = arr.filter(x => x > pivot);
// Recursive quick sort — divide and conquer
return [...this.sort(left), ...middle, ...this.sort(right)];
}
}
class InsertionSortStrategy {
sort(data) {
const arr = [...data];
for (let i = 1; i < arr.length; i++) {
const current = arr[i];
let j = i - 1;
// Shift elements right until the correct position is found
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = current;
}
return arr;
}
}
// Context: the sorter holds a reference to the current strategy
class DataSorter {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
sort(data) {
console.log(`Sorting with: ${this.strategy.constructor.name}`);
const result = this.strategy.sort(data);
console.log(`Result: [${result.join(", ")}]`);
return result;
}
}
const numbers = [64, 34, 25, 12, 22, 11, 90];
const sorter = new DataSorter(new BubbleSortStrategy());
sorter.sort(numbers);
// Output:
// Sorting with: BubbleSortStrategy
// Result: [11, 12, 22, 25, 34, 64, 90]
// Small datasets: insertion sort is fast in practice
sorter.setStrategy(new InsertionSortStrategy());
sorter.sort(numbers);
// Output:
// Sorting with: InsertionSortStrategy
// Result: [11, 12, 22, 25, 34, 64, 90]
// Large datasets: switch to quick sort at runtime
sorter.setStrategy(new QuickSortStrategy());
sorter.sort(numbers);
// Output:
// Sorting with: QuickSortStrategy
// Result: [11, 12, 22, 25, 34, 64, 90]
[IMAGE: Real-world photo of navigation app showing route options (driving, walking, cycling, transit) - search terms: "google maps route options navigation app mobile screen"]
The Connection to the Open/Closed Principle
The Strategy Pattern is the most direct implementation of OCP you'll encounter. Understanding how they connect strengthens both topics in an interview.
OCP says a class should be open for extension but closed for modification. The PaymentProcessor context class demonstrates this perfectly. After the refactor, you never open PaymentProcessor to add new functionality. You extend the system by writing a new strategy class and passing it in. The context is closed. The system is open.
The if-else version violates OCP by definition. Every new payment type forces a modification to the existing checkout() method. Each modification risks breaking the existing paths. Regression test coverage has to grow with every addition.
With the Strategy Pattern, the risk surface of each change is a single new class. Existing strategies are untouched. Existing tests continue to pass unchanged. The new strategy gets its own isolated tests. This is why the pattern appears in almost every OCP discussion in interviews.
Common Mistakes
-
Putting strategy selection logic back into the context. The context should not contain any
if-elsethat selects between strategies. If it does, the pattern hasn't been fully applied. Strategy selection belongs outside the context, in a factory, configuration, or calling code that picks the right strategy and injects it. -
Creating a strategy class for a one-line algorithm. The pattern carries overhead. If the difference between two "strategies" is a single line of code, a simple function reference or a configuration object is the right tool. Strategy Pattern is appropriate when each algorithm is genuinely complex and self-contained.
-
Forgetting that strategies can be stateless. Strategies that hold no data can be reused as singletons. You don't need to create a new
CreditCardStrategyinstance for every checkout if the instance carries no per-transaction state. Stateless strategies are cheaper and simpler. -
Not enforcing the interface contract. In JavaScript, nothing stops a developer from passing an object with no
pay()method as a strategy. Documenting the expected interface clearly, or using TypeScript to enforce it, prevents silent failures at runtime. -
Confusing Strategy with State. Both patterns use a similar structure (context holding a reference to a swappable object) but serve different purposes. Strategy externalizes an algorithm and lets the caller choose it. State changes the context's behavior automatically in response to internal state transitions. The context drives strategy selection. State objects drive themselves.
[INTERNAL-LINK: how State Pattern differs from Strategy → Lesson 8.6: State Pattern]
Interview Questions
Q: What problem does the Strategy Pattern solve?
It removes conditional branching (if-else or switch chains) that grows when multiple algorithms handle the same task differently. Each algorithm is extracted into its own class behind a shared interface. The context holds a reference to the current strategy and delegates to it. Adding a new algorithm means writing a new class only, with no changes to the context.
Q: How does the Strategy Pattern relate to the Open/Closed Principle?
The pattern is a direct implementation of OCP. After applying Strategy, the context class is closed for modification because you never reopen it to add new algorithms. The system is open for extension because new strategy classes can be written and injected without touching existing code. The if-else version violates OCP: every addition requires modifying existing, tested code.
Q: What is the difference between the Strategy Pattern and the State Pattern?
Both use a context that holds a reference to a swappable object and delegates to it. The key difference is who controls the swap and why. In Strategy, the client (caller) selects and injects the strategy from outside. The choice is deliberate and external. In State, the context (or the state objects themselves) trigger state transitions internally in response to actions. The context's behavior changes automatically without the caller's involvement.
Q: Can you use plain functions instead of strategy classes in JavaScript?
Yes. JavaScript treats functions as first-class values, so a strategy can be a function passed directly to the context rather than an object with a method. For simple cases this is cleaner. Strategy classes are preferable when the algorithm needs its own constructor data (like CreditCardStrategy needing card details), when the strategy has multiple related methods, or when the codebase benefits from the explicit naming and structure that classes provide.
Q: Where does the strategy selection happen if not in the context?
Strategy selection typically lives in one of three places: in the calling code (the controller, route handler, or orchestrator that creates the context), in a factory function or factory class that maps a key (like "paypal") to the correct strategy instance, or in a dependency injection container that resolves the correct strategy based on configuration. The goal is to keep the context ignorant of which strategies exist.
Quick Reference Cheat Sheet
STRATEGY PATTERN — QUICK REFERENCE
---------------------------------------------------------------------------
Intent Define a family of algorithms, encapsulate each
in its own class, make them interchangeable at
runtime through a shared interface
Problem solved Growing if-else chains where each branch handles
a different algorithm for the same task
Category Behavioral (Gang of Four)
---------------------------------------------------------------------------
THREE PARTS
---------------------------------------------------------------------------
Strategy Interface / contract that all concrete strategies
Interface must implement (e.g., pay(amount), sort(data))
Concrete Individual classes, each implementing one algorithm
Strategy (CreditCardStrategy, PayPalStrategy, etc.)
Context Holds a reference to the current strategy.
Delegates work to the strategy. Never contains
algorithm logic itself.
---------------------------------------------------------------------------
STRUCTURE (JavaScript)
---------------------------------------------------------------------------
// Strategy interface (documented contract — no formal keyword in JS)
// All strategies must implement: execute(data)
class ConcreteStrategyA {
execute(data) { /* algorithm A */ }
}
class ConcreteStrategyB {
execute(data) { /* algorithm B */ }
}
class Context {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
performTask(data) {
return this.strategy.execute(data); // delegate — no conditions
}
}
---------------------------------------------------------------------------
BEFORE vs AFTER SIGNAL
---------------------------------------------------------------------------
BEFORE (violation): checkout() has if-else with 4+ branches
Each branch is a different algorithm
Adding a method = editing checkout()
AFTER (correct): checkout() has 3 lines
Each algorithm is a separate class
Adding a method = writing a new class only
---------------------------------------------------------------------------
SOLID CONNECTIONS
---------------------------------------------------------------------------
OCP Context is closed for modification, open for extension
SRP Each strategy class has one responsibility: its algorithm
DIP Context depends on the strategy interface (abstraction),
not on concrete strategy classes directly
---------------------------------------------------------------------------
WHEN TO USE
---------------------------------------------------------------------------
- Multiple algorithms for the same task that should be swappable
- You need to add new algorithms without touching existing code
- Algorithm selection should be configurable at runtime
- You want each algorithm independently testable in isolation
WHEN NOT TO USE
---------------------------------------------------------------------------
- Only two algorithms that will never grow — simple if-else is fine
- The algorithm difference is a single line — use a config value
- The overhead of multiple classes is not justified by complexity
---------------------------------------------------------------------------
STRATEGY vs STATE (INTERVIEW TRAP)
---------------------------------------------------------------------------
Strategy: client selects and injects the algorithm from outside
State: context or state objects trigger transitions internally
Strategy: algorithms are interchangeable on the same task
State: behavior changes as the object's internal state changes
---------------------------------------------------------------------------
Previous: Lesson 8.4 - Builder Pattern Next: Lesson 8.6 - State Pattern
This is Lesson 8.5 of the OOP Interview Prep Course — 8 chapters, 41 lessons.