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.

Identify the if-else block

Find a conditional that switches on a “type” or “mode”. Each branch is a candidate algorithm.

The condition itself is what you will eliminate.

Extract the shared contract

Look at what every branch does. Name the common operations — usually 1–3 methods.
That becomes your Strategy interface. Don’t overthink it.

Move each branch to its own class

Cut each if body, paste it into a new ConcreteStrategy class that implements the interface. One branch → one class. Name it clearly.

Inject the strategy into the Context

Replace the old conditional caller with a setStrategy() or a registry map. The caller resolves which strategy to use; the Context just executes it.

🚀 THE PAYOFF

After the refactor: you can add 10 more payment methods by adding 10 new files. The PaymentProcessor class is untouched — it doesn’t even know you added them. This is how real systems scale without becoming legacy nightmares.

Real-World Applications

The Strategy pattern is not a textbook abstraction. It is running inside the systems you use every day.

Razorpay / PhonePe - Payment Gateway Routing

Every payment method (UPI, cards, wallets, BNPL) is an isolated strategy implementation. The payment orchestrator swaps between them based on user selection without its core logic changing. This is how Razorpay added 100+ payment methods without rewriting the checkout engine.

Swiggy / Zomato — Delivery Route Optimisation

During normal hours, a standard shortest-path algorithm runs. During a Diwali rush, a traffic-aware real-time strategy kicks in. During rain, a modified ETA-safety strategy takes over. One RouteOptimizer context, three strategies — switched on demand.

Auth Systems - Authentication Strategy

Password login, OTP login, OAuth via Google, biometric fingerprint — every auth method implements a shared AuthStrategy interface. The login controller calls authenticate() regardless of which method is active. Spring Security’s AuthenticationProvider is a direct expression of this pattern.

Sorting & Data Processing Pipelines

Java’s Collections.sort(list, comparator) is textbook Strategy. The Comparator is the strategy interface. You pass in any comparison logic without modifying the sorting algorithm itself. Same idea powers Python’s sorted(key=fn) and most data pipeline frameworks.

Recommendation Engines

New user with no history? Use the popularity-based strategy. Returning user with purchase history? Use the collaborative-filtering strategy. A/B test running? Use the experimental ML strategy. Netflix, Meesho, and Amazon all route users through different recommendation strategies at runtime.

When to Use (and When NOT to Use) Strategy

✅ Use Strategy when…

You have multiple variants of the same algorithm and switching between them should not require changing the calling code. You want to eliminate large conditional blocks that grow with every new variant. You need to be able to swap behaviour at runtime, not just at compile time. You want each variant independently testable with zero infrastructure.

🚫 Avoid Strategy when…

You only have one algorithm and there is no realistic expectation of it changing. The number of strategies is truly fixed and small (two variants, forever). The overhead of creating extra classes and interfaces exceeds the benefit — for a 20-line script or a prototype, Strategy is over-engineering.

⚠ THE OVER-ENGINEERING TRAP

Not every if-else deserves a Strategy pattern. If a condition has two branches and neither is expected to grow, pulling it into two classes and an interface adds complexity without value. Apply Strategy when you are designing for a clearly extensible dimension — not as a reflex. 

How Strategy Shows Up in SDE Interviews

At product companies like Amazon, Flipkart, Meesho, and Razorpay, LLD rounds routinely involve designing a system where Strategy is the natural fit. Common prompts:

Interview Prompt Strategy Application
“Design a payment system” Each payment method is a strategy
“Design a file compression tool” ZIP, GZIP, BZIP2 are strategies
“Design a logging framework” Console, File, Cloud are strategies
“Design a discount engine” Flat, Percent, BuyOneGetOne are strategies
“Design a notification service” Email, SMS, Push are strategies
“Design a search ranking system” Relevance, Price, Popularity are strategies
📌 INTERVIEW TIP

When you introduce Strategy in an interview, always explain it in two sentences first: “Each algorithm gets its own class that implements a shared interface. The caller holds a reference to the interface, so we can swap implementations without touching the calling code.” Then draw the UML. Interviewers respond very well to this structure.

⚠️Common Mistakes Developers Make
Good design is not about using patterns everywhere — it's about using them where they add value.

If you want to explore more design patterns and system design concepts, you can browse additional guides on https://blog.codekerdos.in/.

📌 Strategy Design Pattern Interview Questions (FAQs)

1. What is the Strategy Design Pattern in simple terms?

The Strategy Design Pattern is a behavioral design pattern that allows you to define multiple algorithms (or behaviors), encapsulate each one in a separate class, and make them interchangeable at runtime.

Instead of using large if-else or switch statements, you delegate the behavior to objects that implement a common interface.

2. When should I use the Strategy Pattern?

Use the Strategy Pattern when:

3. When should I NOT use the Strategy Pattern?

Avoid using Strategy Pattern when:

In such cases, a simple conditional statement is often sufficient.

4. How does Strategy Pattern relate to SOLID principles?

Strategy Pattern strongly supports:

5. What is the difference between Strategy Pattern and Factory Pattern?

👉 In many real systems, Factory is used to select and provide the correct Strategy

6. What is the difference between Strategy Pattern and State Pattern?

👉 Strategy = “Choose behavior”
👉 State = “Behavior changes automatically”

7. How does Strategy Pattern help in LLD interviews?

In LLD interviews, Strategy Pattern helps you:

A good signal to use Strategy is when the problem involves multiple interchangeable behaviors.

8. Can Strategy Pattern impact performance?

Strategy Pattern introduces a small overhead due to:

However, this overhead is usually negligible compared to the benefits of:

9. How do real systems manage multiple strategies efficiently?

In production systems, strategies are often managed using:

This avoids repeatedly creating objects and enables efficient runtime selection.

10. How do I identify Strategy Pattern in existing code?

Look for:

👉 These are strong indicators that Strategy Pattern can improve the design.

Scroll to Top