DEV Community

Timevolt
Timevolt

Posted on

The Matrix: Choosing the Right Strategy Pattern

The Quest Begins (The "Why")

Honestly, I used to dread adding a new payment method to our checkout flow. Every time the product team said, “Let’s support Apple Pay next sprint,” I felt a knot in my stomach. Why? Because the code looked like this:

function processPayment(order) {
  if (order.method === 'credit_card') {
    return chargeCreditCard(order);
  } else if (order.method === 'paypal') {
    return chargePayPal(order);
  } else if (order.method === 'apple_pay') {
    return chargeApplePay(order);
  } else {
    throw new Error('Unsupported payment method');
  }
}
Enter fullscreen mode Exit fullscreen mode

It seemed harmless at first—a tidy if/else chain. But as we added more methods (Google Pay, crypto, even store credit), the function ballooned. Adding a new branch meant touching the same file, risking a regression in every other branch. I’d spend hours tracing why a change in the crypto block broke PayPal for some edge case. The code felt like a maze where every turn could lead to a dead‑end, and I was the one holding the torch.

I kept thinking, “There has to be a better way.” The moment I realized the problem wasn’t the number of payment methods—it was the way we were selecting them—I knew I was on the right track.

The Revelation (The Insight)

The “aha!” came when I read about the Strategy pattern. Instead of embedding the algorithm directly in the caller, you define a family of interchangeable strategies, encapsulate each one, and let the client pick the strategy at runtime. The context (the object that uses the strategy) doesn’t need to know how the work gets done—only that it follows a common interface.

In plain English: stop asking “What method is this?” and start saying “Give me the object that knows how to do this.” The decision of which strategy to use moves out of the core logic and into a factory or configuration layer. The core logic becomes blissfully simple, and adding a new payment method is just adding a new class—no touching existing code.

It felt like unlocking a new ability in Zelda when the code finally clicked. Suddenly, the payment processing flow was no longer a tangled knot of conditionals; it was a clean delegation to a strategy object.

Wielding the Power (Code & Examples)

Before – The Struggle

// paymentProcessor.js
function processPayment(order) {
  if (order.method === 'credit_card') {
    return chargeCreditCard(order);
  } else if (order.method === 'paypal') {
    return chargePayPal(order);
  } else if (order.method === 'apple_pay') {
    return chargeApplePay(order);
  } else if (order.method === 'google_pay') {
    return chargeGooglePay(order);
  } else if (order.method === 'store_credit') {
    return applyStoreCredit(order);
  } else {
    throw new Error('Unsupported payment method');
  }
}
Enter fullscreen mode Exit fullscreen mode

Problems:

  • Every new payment method forces a edit in this file.
  • Unit testing requires mocking all branches to isolate one.
  • A typo in one condition can silently break another method (imagine accidentally using === 'paypa l').
  • The function violates the Open/Closed Principle: it’s open for modification, closed for extension.

After – The Victory

First, define the strategy interface:

// paymentStrategy.js
class PaymentStrategy {
  pay(order) {
    throw new Error('pay method must be implemented');
  }
}
Enter fullscreen mode Exit fullscreen mode

Then concrete strategies:

// creditCardStrategy.js
class CreditCardStrategy extends PaymentStrategy {
  pay(order) {
    return chargeCreditCard(order);
  }
}

// paypalStrategy.js
class PayPalStrategy extends PaymentStrategy {
  pay(order) {
    return chargePayPal(order);
  }
}

// applePayStrategy.js
class ApplePayStrategy extends PaymentStrategy {
  pay(order) {
    return chargeApplePay(order);
  }
}
Enter fullscreen mode Exit fullscreen mode

And a simple factory to pick the right strategy (this could be a DI container, a config map, etc.):

// paymentFactory.js
const strategies = {
  credit_card: new CreditCardStrategy(),
  paypal: new PayPalStrategy(),
  apple_pay: new ApplePayStrategy(),
  // add more here without touching the processor
};

function getPaymentStrategy(method) {
  const strat = strategies[method];
  if (!strat) throw new Error(`Unsupported payment method: ${method}`);
  return strat;
}
Enter fullscreen mode Exit fullscreen mode

Finally, the processor becomes a thin delegate:

// paymentProcessor.js
function processPayment(order) {
  const strategy = getPaymentStrategy(order.method);
  return strategy.pay(order);
}
Enter fullscreen mode Exit fullscreen mode

What changed?

  • Adding a new method means creating a new class that implements PaymentStrategy and registering it in the factory. No existing file is touched.
  • Each strategy can be tested in isolation—just instantiate the class and call pay.
  • The processor now has a single responsibility: delegate. It’s easy to read, easy to reason about, and easy to extend.

Common Traps to Avoid

  1. Putting logic inside the factory – If the factory starts deciding how to pay based on order details, you’ve leaking strategy knowledge back into the creation layer. Keep the factory dumb: it only maps a key to an object.
  2. Forgetting the interface – In JavaScript it’s tempting to skip an explicit base class. Still, document the expected method signature (perhaps with JSDoc) so future contributors know what a strategy must implement.

Why This New Power Matters

Adopting the Strategy pattern didn’t just clean up a single file—it shifted how I think about variability in code. Whenever I see a branching statement that selects behavior based on a flag, I ask myself: Could this be a strategy? The answer is often yes, and the payoff is immediate:

  • Safety: Adding new behavior never risks breaking existing behavior.
  • Clarity: The core algorithm reads like a story: “Get the right tool, then use it.”
  • Testability: Each strategy is a tiny, self‑contained unit you can unit‑test without mocking half the system.
  • Team velocity: New teammates can add a payment method by following a clear pattern, no tribal knowledge required.

It’s like swapping out a rusty sword for a lightsaber—you still fight the same enemies, but the swing is smoother, the hum is satisfying, and you feel ready for the next boss battle.

Your Turn

Give it a spin! Take a conditional mess in your codebase—maybe a notification sender, a logging formatter, or a UI component that switches on a prop—and extract the varying bits into strategies. Start with an interface, write two concrete strategies, and wire them up with a simple factory.

How did it feel when the if/else chain vanished? Drop a comment below; I’d love to hear your war stories (and maybe swap a few strategy tips). Happy coding!

Top comments (0)