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 } }