A while ago, I opened a file and immediately knew something was off.
Not because it was broken.
Not because it was complex.
But because it had that shape.
You know the one:
if (...) {
// something
} else if (...) {
// something else
} else if (...) {
// another thing
} else {
// fallback
}
It worked.
But every time we needed to add a new case, we had to go back into that block and make it just a bit worse.
And that’s when I realized:
This isn’t just messy code. This is a scaling problem.
Let’s fix it.
TL;DR
- Long
if-elsechains are a signal that your logic doesn’t scale well. - The Strategy Pattern helps you encapsulate behaviors and swap them easily.
- In JavaScript, you don’t need heavy OOP: functions are often enough.
Table of Contents
- The Problem: The If-Else Monster
- A Real Example: Payment Methods
- Why This Doesn’t Scale
- Introducing the Strategy Pattern
- Step-by-Step Refactoring
- Final Result: Clean and Extensible
- Using TypeScript for Safer Strategies
- Optional: Class-Based Strategy
- When to Use (and Not Use) Strategy
- Final Thoughts
The Problem: The If-Else Monster
At first, if-else feels fine.
It’s simple.
It’s readable.
It gets the job done.
Until it doesn’t.
The moment your logic starts growing based on types, modes, or variants, things start getting messy.
And more importantly:
every new feature requires modifying existing logic
That’s the real problem.
A Real Example: Payment Methods
Let’s say you’re building a checkout system.
You need to handle multiple payment methods:
- credit card
- PayPal
- bank transfer
A typical implementation might look like this:
function processPayment(method, amount) {
if (method === "credit-card") {
console.log("Processing credit card payment:", amount)
// card logic
} else if (method === "paypal") {
console.log("Processing PayPal payment:", amount)
// paypal logic
} else if (method === "bank-transfer") {
console.log("Processing bank transfer:", amount)
// bank logic
} else {
throw new Error("Unsupported payment method")
}
}
It works, but you can already feel the tension.
Why This Doesn’t Scale
The issue isn’t the code itself.
It’s how it evolves.
Every time you add a new payment method:
- you modify this function
- you increase its complexity
- you risk breaking existing logic
After a few iterations, you end up with:
- long functions
- duplicated patterns
- fragile logic
This is exactly where the Strategy Pattern shines.
Introducing the Strategy Pattern
The idea is simple:
Instead of having one function decide how to do everything,
delegate each behavior to a separate “strategy”.
Each strategy handles one case.
The main function just selects which one to use.
In practice:
replace conditionals with a lookup-based approach
Step-by-Step Refactoring
Let’s improve the previous example step by step.
Step 1: Extract behaviors
function payWithCreditCard(amount) {
console.log("Processing credit card payment:", amount)
}
function payWithPaypal(amount) {
console.log("Processing PayPal payment:", amount)
}
function payWithBankTransfer(amount) {
console.log("Processing bank transfer:", amount)
}
Each function now has a clear responsibility.
Step 2: Create a strategy map
const paymentStrategies = {
"credit-card": payWithCreditCard,
"paypal": payWithPaypal,
"bank-transfer": payWithBankTransfer,
}
This replaces the if-else chain with a simple lookup.
Step 3: Simplify the main function
function processPayment(method, amount) {
const strategy = paymentStrategies[method]
if (!strategy) {
throw new Error("Unsupported payment method")
}
strategy(amount)
}
No branching logic.
Just delegation.
Final Result: Clean and Extensible
Now adding a new payment method is straightforward:
function payWithCrypto(amount) {
console.log("Processing crypto payment:", amount)
}
paymentStrategies["crypto"] = payWithCrypto
No need to modify existing logic.
No risk of breaking unrelated behavior.
👉 This is the real benefit: open for extension, closed for modification
Using TypeScript for Safer Strategies
So far, everything works perfectly in JavaScript.
And thanks to this guard:
if (!strategy) {
throw new Error("Unsupported payment method")
}
invalid inputs are handled safely.
However, the error is still detected at runtime.
With TypeScript, we can catch the same issue earlier, before the code even runs.
Step 1: Define allowed methods
type PaymentMethod = "credit-card" | "paypal" | "bank-transfer"
Now only valid methods are allowed.
Step 2: Type the strategies
type PaymentStrategy = (amount: number) => void
const paymentStrategies: Record<PaymentMethod, PaymentStrategy> = {
"credit-card": payWithCreditCard,
"paypal": payWithPaypal,
"bank-transfer": payWithBankTransfer,
}
Now TypeScript guarantees:
- every method has a strategy
- every strategy has the correct signature
Step 3: Type the main function
function processPayment(method: PaymentMethod, amount: number) {
const strategy = paymentStrategies[method]
strategy(amount)
}
No need for runtime checks anymore.
Errors are caught at compile time.
Why this matters
With JavaScript, you make your code flexible.
With TypeScript, you make it reliable.
You’re not just cleaning your code.
You’re making invalid states impossible.
Optional: Class-Based Strategy
If you prefer a more OOP-style approach with JavaScript, you can model strategies as classes:
class CreditCardStrategy {
execute(amount) {
console.log("Processing credit card payment:", amount)
}
}
class PayPalStrategy {
execute(amount) {
console.log("Processing PayPal payment:", amount)
}
}
Then use them like this:
const strategies = {
"credit-card": new CreditCardStrategy(),
"paypal": new PayPalStrategy(),
}
function processPayment(method, amount) {
const strategy = strategies[method]
if (!strategy) {
throw new Error("Unsupported payment method")
}
strategy.execute(amount)
}
It’s more structured, but also more verbose.
In JavaScript, the functional approach is usually enough.
When to Use (and Not Use) Strategy
Use it when:
- you have multiple variants of the same behavior
- new cases are added frequently
- conditionals are growing
Avoid it when:
- you only have a couple of simple cases
- the logic is unlikely to change
- it adds unnecessary complexity
The goal is not to apply patterns, but to solve real problems.
Final Thoughts
The Strategy Pattern isn’t about being clever, it’s about writing code that can grow without becoming fragile.
Most messy if-else chains don’t start messy, they become messy over time and by the time you notice, changing them feels risky.
The good news is:
You don’t need a rewrite, just a better way to organize behavior.
If this helped you:
- Leave a ❤️ reaction
- Drop a 🦄 unicorn
- Share the worst
if-elsemonster you’ve seen
And if you enjoy this kind of content, follow me here on DEV for more.
Top comments (6)
Great explanation of how the Strategy Pattern can simplify complex conditional logic, the examples really make the benefits clear. I especially like how you highlighted the flexibility it brings when adding new behaviors without touching existing code.
One thing I’m curious about: how would you decide between using the Strategy Pattern and a simple object map of functions in JavaScript for smaller cases? Do you see one approach scaling better in real-world applications?
Thank you @lucaferri , I’m glad the examples resonated! That’s a really insightful question, because in JavaScript the line between a “Strategy Pattern” and a simple object map can get blurry.
For smaller cases, I’d usually start with an object map of functions. It’s lightweight, very readable, and fits naturally with how JS handles first-class functions. In fact, many “strategy-like” implementations in JS are just that, an object where each key maps to a function, and you select one at runtime.
Where the Strategy Pattern starts to shine is when the problem grows in complexity. A few signals I look for:
At that point, wrapping strategies in objects/classes gives you better structure, testability, and scalability.
So in practice, I see it less as “one vs the other” and more as a progression:
In terms of scaling, the Strategy Pattern generally holds up better in larger codebases because it makes behavior explicit and composable, whereas large object maps can become harder to reason about as they grow.
A good rule of thumb I use:
Curious to hear how others approach this trade-off too—especially in TypeScript-heavy projects.
Glad that made sense. I really like the way you framed it as a progression rather than a strict either/or 👍
I tend to follow a similar path in practice. I’ll almost always start with an object map because it keeps things simple and avoids over-engineering early on. In many cases, that’s honestly all you ever need.
Where I’ve seen things shift is exactly where you pointed out: once behaviors start accumulating logic, dependencies, or state, the map can quietly become harder to reason about. At that point, moving to a more explicit Strategy-style structure feels less like adding complexity and more like making the existing complexity visible and manageable.
I also appreciate your point about TypeScript: having a shared contract really nudges things toward a Strategy approach, especially in larger teams where consistency matters.
So yeah, your rule of thumb resonates a lot:
That balance between pragmatism and scalability is really the key.
Thank you @gavincettolo
Really enjoyed this breakdown, especially how you framed the Strategy Pattern as a way to pay down conditional complexity over time instead of just “cleaning up if/else.” That nuance often gets lost.
One thing that stood out to me is how naturally this maps to systems that need runtime flexibility. In distributed environments (thinking feature flags, multi-tenant behavior, or even traffic routing), being able to swap strategies without touching the calling code is incredibly powerful, and aligns well with the idea that strategies are interchangeable algorithms selected at runtime ().
I’m curious how you think about the boundary between Strategy and over-engineering, though. In practice, I’ve seen teams introduce the pattern early, only to end up with a “strategy explosion” where each variation becomes its own class/function, even when a simple conditional might have been more readable (a concern that comes up often in community discussions too).
So my question for you:
👉 What heuristics do you use to decide when to introduce a Strategy vs. sticking with a straightforward conditional, especially in smaller code paths that might evolve later?
Also, how would you approach strategy selection in a dynamic system (e.g., per-request or per-user)? Would you lean toward a factory/DI approach, or keep the selection closer to the call site to preserve clarity?
Would love to hear how you’ve handled that trade-off in real-world projects.
Great points, @elenchen, I really appreciate the thoughtful take (and the distributed systems angle).
You’re absolutely right that the Strategy Pattern can become overkill if introduced too early. I tend to follow a pretty simple rule of thumb:
I like how you framed it as “paying down conditional complexity over time”—that’s exactly how I think about it too. It’s less about upfront “clean code” and more about creating a structure that can absorb change without becoming brittle.
On the “strategy explosion” problem: totally agree. I’ve seen that happen when every tiny variation gets abstracted prematurely. One thing that helps me is asking: Is this variation a real domain concept, or just an implementation detail?
If it’s not meaningful at the domain level, I usually avoid turning it into its own strategy.
Unfortunately, I'm not an expert in dynamic systems, so I'll leave comments on this topic to other developers with more expertise in the field.
That said, early on, I’ll often keep the selection closer to the call site for clarity, like you mentioned. It’s a trade-off between explicitness vs. extensibility, and the “right” choice tends to evolve with the system.
Really liked your point about feature flags and routing, those are perfect real-world examples where Strategy fits naturally.
Thanks again for the great comment
Thanks again for the thoughtful response and for taking the time to engage 🙌 Curious to see what others will add to that part of the conversation!