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.
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.
— Gang of Four, Design Patterns (1994)
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.
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().
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.
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 |
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); }
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); } }
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 } }
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);
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.
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).
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.
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.
Find a conditional that switches on a “type” or “mode”. Each branch is a candidate algorithm.
The condition itself is what you will eliminate.
Look at what every branch does. Name the common operations — usually 1–3 methods.
That becomes your Strategy interface. Don’t overthink it.
Cut each if body, paste it into a new ConcreteStrategy class that implements the interface. One branch → one class. Name it clearly.
Replace the old conditional caller with a setStrategy() or a registry map. The caller resolves which strategy to use; the Context just executes it.
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.
The Strategy pattern is not a textbook abstraction. It is running inside the systems you use every day.
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.
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.
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.
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.
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.
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.
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.
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.
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 |
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.
If you want to explore more design patterns and system design concepts, you can browse additional guides on https://blog.codekerdos.in/.
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.
Use the Strategy Pattern when:
Avoid using Strategy Pattern when:
In such cases, a simple conditional statement is often sufficient.
Strategy Pattern strongly supports:
👉 In many real systems, Factory is used to select and provide the correct Strategy
👉 Strategy = “Choose behavior”
👉 State = “Behavior changes automatically”
In LLD interviews, Strategy Pattern helps you:
A good signal to use Strategy is when the problem involves multiple interchangeable behaviors.
Strategy Pattern introduces a small overhead due to:
However, this overhead is usually negligible compared to the benefits of:
In production systems, strategies are often managed using:
This avoids repeatedly creating objects and enables efficient runtime selection.
Look for:
👉 These are strong indicators that Strategy Pattern can improve the design.