• Home /
  • Gen AI /
  • The Strategy Design Pattern: Stop Writing if-else Chains Forever

The Strategy Design Pattern: Stop Writing if-else Chains Forever

In this blog, we’ll understand how the Strategy Design Pattern in Java helps you replace messy if-else chains with a cleaner and more scalable approach—both in LLD interviews and real-world systems.

Picture this scenario.

You are shipping a feature for your e-commerce app. Users can now pay via credit card, UPI, or net banking. 

So you write it:

public void processPayment(String type, double amount) {
    if (type.equals("CREDIT_CARD")) {
        // validate card, call Stripe, log transaction...
    } else if (type.equals("UPI")) {
        // validate VPA, call UPI gateway...
    } else if (type.equals("NET_BANKING")) {
        // bank list, redirect, callback...
    }
    // next quarter: add PayPal -> edit this file again
    // then crypto -> edit this file again
}

It ships. Life is good — for three months. Then the product team adds two more payment methods. Then a third. The method balloons to 285 lines. Nobody wants to touch it anymore. One wrong keystroke in the UPI block can silently break the credit card flow. Your unit tests are a nightmare.

⚠ Before Strategy Pattern — What Your Code Becomes

if (type == "CREDIT_CARD") { ... 40 lines ... }
else if (type == "UPI") { ... 35 lines ... }
else if (type == "NET_BANKING") { ... 50 lines ... }
else if (type == "PAYPAL") { ... 45 lines ... } ← added Q2
else if (type == "CRYPTO") { ... 60 lines ... } ← added Q3
else if (type == "BNPL") { ... 55 lines ... } ← added Q4

Touch one branch, pray the others survive.

This is the exact problem the Strategy Pattern was designed to solve.
"Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it."

— Gang of Four, Design Patterns (1994)

💡 The Real Insight

Strategy Pattern is not about adding classes. It’s about removing decisions from your code.

If your code keeps asking “what type is this?” at runtime — through instanceof, string comparisons, or enum switches — that’s your codebase screaming for Strategy. Every if (type == X) is a decision that could live inside a class instead.

What Is the Strategy Pattern?

The Strategy pattern is a behavioural design pattern. That word “behavioural” is key — it is about how objects communicate and how responsibilities are distributed, not about how they are created (Creational) or how they are structured (Structural).

The core idea: pull each variant of an algorithm out of a conditional chain, wrap it in its own class, and plug it in through a shared interface. The caller — called the Context — never needs to know which specific algorithm is running. It just calls execute().

📌 NOTE

The Strategy pattern is one of the most commonly asked Low-Level Design (LLD) questions at top product companies like Amazon, Flipkart, and Razorpay. If you have an SDE interview in your future, this pattern is not optional. 

The Participants — A Visual Map

Before we touch code, let us get the vocabulary right. The Strategy pattern has exactly three moving parts:

Role

Responsibility

In our payment example

Context

Holds a reference to the active strategy. Delegates execution to it.

PaymentProcessor

Strategy (interface)

Defines the contract — what every algorithm must be able to do.

PaymentStrategy

Concrete Strategy

One class per algorithm variant. Implements the interface.

CreditCardStrategy, UpiStrategy

✗ Before Strategy
One bloated method
✓ After Strategy
Plug-and-play algorithms
All algorithms crammed into if-else Each algorithm = its own class
One change can break everything Changes are fully isolated
New payment = edit existing file New payment = add one new file
Can’t unit test algorithms in isolation Each strategy is independently testable
Grows without bound — forever Context stays lean — always

Building It Step by Step

Step 1: Define the Strategy Interface

First, we identify the common contract. Every payment method needs to do two things: validate the input and process the payment. That goes into an interface.

PaymentStrategy.java

// The shared contract -- every payment method must implement this
public interface PaymentStrategy {
    boolean validate(PaymentRequest request);
    PaymentResult pay(double amount, PaymentRequest request);
}
Step 2: Write a Concrete Strategy per Algorithm

Each payment method becomes its own class. It only knows about its own logic. Changes to UPI logic cannot possibly break the credit card class — they live in completely separate files.

CreditCardStrategy.java 

public class CreditCardStrategy implements PaymentStrategy {

    private final StripeGateway stripe;

    public CreditCardStrategy(StripeGateway stripe) {
        this.stripe = stripe;
    }

    @Override
    public boolean validate(PaymentRequest req) {
        // Luhn check, expiry validation, CVV length...
        return LuhnAlgorithm.check(req.getCardNumber());
    }

    @Override
    public PaymentResult pay(double amount, PaymentRequest req) {
        return stripe.charge(amount, req.getToken());
    }
}

UpiStrategy.java 

public class UpiStrategy implements PaymentStrategy {

    private final NpciGateway npci;

    @Override
    public boolean validate(PaymentRequest req) {
        // VPA format check: user@bank
        return req.getVpa().matches("^[a-zA-Z0-9.]+@[a-zA-Z]+$");
    }

    @Override
    public PaymentResult pay(double amount, PaymentRequest req) {
        return npci.initiateTransfer(req.getVpa(), amount);
    }
}
Step 3: Build the Context

The Context holds a reference to whichever strategy is active. It delegates the work without caring about the implementation details. This is the entire magic of the pattern.

PaymentProcessor.java (Context) 

public class PaymentProcessor {

    private PaymentStrategy strategy; // set at runtime

    // Strategy is injected -- not hardcoded
    public void setStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public PaymentResult processPayment(double amount, PaymentRequest request) {
        if (!strategy.validate(request)) {
            throw new InvalidPaymentException("Validation failed");
        }
        return strategy.pay(amount, request);
        // Works identically for CreditCard, UPI, BNPL, Crypto -- anything
    }
}
Step 4: Wire It All at the Call Site

At runtime — perhaps in your service layer or a factory — you decide which strategy to use. The PaymentProcessor is blissfully unaware of this decision.

CheckoutService.java 

PaymentProcessor processor = new PaymentProcessor();

// User selects UPI at checkout
processor.setStrategy(new UpiStrategy(npciGateway));
processor.processPayment(499.00, request);

// Corporate user selects credit card -- swap the strategy, same processor
processor.setStrategy(new CreditCardStrategy(stripeGateway));
processor.processPayment(14999.00, request);

// Tomorrow, product adds crypto payments -- create ONE new class, zero changes here
processor.setStrategy(new CryptoStrategy(web3Gateway));
processor.processPayment(0.05, request);
✅ WHAT CHANGED?

Adding crypto payments required creating exactly one new file — CryptoStrategy.java. Zero edits to PaymentProcessor. Zero risk to existing payment flows. This is the Open/Closed Principle enforced at the architectural level.

The Real-Engineering Upgrade: Strategy Registry

In production systems, engineers don’t call setStrategy() manually every time. They build a strategy registry — a map from a key to a strategy — and resolve the right one at runtime. This is how Razorpay, Paytm, and every serious payment system actually looks under the hood:

StrategyRegistry — Real Engineering Thinking

// Register all strategies once -- at startup or via DI framework
Map<String, PaymentStrategy> strategies = Map.of(
    "UPI",         new UpiStrategy(npciGateway),
    "CREDIT_CARD", new CreditCardStrategy(stripeGateway),
    "NET_BANKING", new NetBankingStrategy(hdfc),
    "BNPL",        new BnplStrategy(simpl),
    "CRYPTO",      new CryptoStrategy(web3Gateway)
);

// At checkout -- resolve from user's selection, no if-else anywhere
String userChoice = request.getPaymentMethod(); // e.g. "UPI"
PaymentStrategy strategy = strategies.get(userChoice);

if (strategy == null) {
    throw new UnsupportedPaymentException("Unknown method: " + userChoice);
}

PaymentResult result = strategy.pay(order.getTotal(), request);
// No switch. No if-else. Just a map lookup. Clean, scalable, O(1).
💡 ENGINEERING INSIGHT

The registry pattern is Strategy + a factory built into a Map. Adding a new payment method in production literally means adding one line to this map and one new class file. No existing logic is touched. Zero regression risk. This is the pattern inside Spring’s HandlerMapping, Kafka’s deserializer registry, and most plugin architectures.

Refactoring to Strategy — Step by Step

Already have a giant if-else in your codebase? Here’s exactly how to migrate it. This is the journey every engineer who has truly internalized Strategy goes through. And you can start it on your very next sprint.

Scroll to Top