DEV Community

Cover image for The Strategy Pattern in JavaScript: Replace Messy If-Else Logic With Clean Code

The Strategy Pattern in JavaScript: Replace Messy If-Else Logic With Clean Code

Gavin Cettolo on April 14, 2026

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 th...
Collapse
 
lucaferri profile image
Luca Ferri

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?

Collapse
 
gavincettolo profile image
Gavin Cettolo

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:

  • Shared interface or contract becomes important (especially with TypeScript)
  • Strategies need internal state or dependencies
  • You want to enforce separation of concerns more strictly
  • You expect the behavior set to grow over time or be extended by others

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:

  • Object map → great for simple, static behavior switching
  • Strategy Pattern → better for complex, evolving, or extensible systems

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:

Start simple with a function map, and refactor to Strategy when the logic starts needing structure.

Curious to hear how others approach this trade-off too—especially in TypeScript-heavy projects.

Collapse
 
lucaferri profile image
Luca Ferri

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:

start simple, then refactor when structure is actually needed

That balance between pragmatism and scalability is really the key.
Thank you @gavincettolo

Collapse
 
elenchen profile image
Elen Chen

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.

Collapse
 
gavincettolo profile image
Gavin Cettolo

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:

  • If I have 1-2 simple branches, I stick with conditionals
  • Once I hit 3+ variations and the logic starts to diverge or grow independently, that’s when Strategy starts to pay off
  • Especially if I know new variants are coming or the behavior needs to change at runtime

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

Collapse
 
elenchen profile image
Elen Chen

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!

Collapse
 
paolozero profile image
Paolo Zero

Great read 👏

I like how you framed the Strategy Pattern as a practical way to eliminate messy conditionals—this is exactly where it shines. The idea of encapsulating interchangeable behaviors and selecting them at runtime is such a powerful shift from rigid if/else chains, especially in growing codebases.

One thing I’ve learned building real products is that the real challenge isn’t just implementing strategies, but deciding where the strategy selection should live. If that logic leaks into multiple places, you risk just “moving the if/else elsewhere” instead of truly simplifying the design. I’ve found combining Strategy with a small factory or dependency injection layer keeps things clean and scalable.

Also appreciated the JavaScript-friendly approach—using functions as strategies keeps things lightweight without overengineering with classes.

Curious how you’d handle cases where strategies need shared state or async dependencies (e.g., API clients)? That’s usually where things get interesting in real-world apps.

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thanks a lot, @paolozero, really thoughtful comment, I appreciate it 🙌

You nailed one of the trickiest parts: where the strategy selection actually lives. I’ve definitely fallen into that trap before, thinking I cleaned things up, but really just relocated the conditional logic. Lately, I’ve been leaning toward exactly what you mentioned: a thin factory layer or even a configuration-driven map to keep that decision centralized and predictable.

On shared state and async dependencies, that’s where things can get messy fast in JavaScript. In most real-world cases, I treat strategies as pure as possible and inject what they need (API clients, configs, etc.) from the outside. When that’s not feasible, I’ll sometimes wrap strategies in closures or use a class-based approach to manage state more explicitly.

It’s a bit of a trade-off between simplicity and flexibility, and honestly, I don’t think there’s a one-size-fits-all answer, just patterns that evolve with the complexity of the app.

Really glad the article resonated with your experience building real products, that’s exactly the perspective I was hoping to connect with.

Collapse
 
lucaferri profile image
Luca Ferri

Hi @gavincettolo, in a future article can you talk about the Abstract Factory pattern?
I love the way you explain things and I have never fully understood this OOP pattern.
Thank you!

Collapse
 
gavincettolo profile image
Gavin Cettolo

Thank you @lucaferri for your feedback. Sure, it is on the list 🙂

Collapse
 
gavincettolo profile image
Gavin Cettolo

Let me know if there are any patterns you want me to write about 🙂