Published
- 5 min read
Strategy Pattern in Java and Spring: Add PayLater Without Touching Checkout
One interface, one class per payment, and a Spring map that wires them all for you.
Your interviewer asks you to add a new payment type, PayLater, without touching any existing code. How would you do it?
This question comes up in low-level design (LLD) rounds because it tests one idea: can you design code that grows by adding classes instead of editing old ones? The answer is the Strategy pattern, and if you write Java, you’ve already used it.
The if-else version
Most of us start like this: one pay method with a branch for each payment type.
void pay(Type t, long amt) {
if (t == Type.CARD) {
card(amt);
} else if (t == Type.PAYPAL) {
payPal(amt);
} else if (t == Type.WALLET) {
wallet(amt);
}
}
It works. Now add PayLater:
void pay(Type t, long amt) {
if (t == Type.CARD) {
card(amt);
} else if (t == Type.PAYPAL) {
payPal(amt);
} else if (t == Type.WALLET) {
wallet(amt);
} else if (t == Type.PAY_LATER) {
payLater(amt);
}
}
Every new payment type means editing this class, and everything in it ships again: the card, PayPal and wallet paths you never meant to change need retesting too. The class is never finished. In design terms it breaks the open/closed principle: code should be open for extension but closed for modification.
The Strategy pattern
Move each branch into its own class, behind one interface:
interface PaymentStrategy {
void pay(long amt);
}
amt is in cents, so $49 is pay(4900). Each payment becomes a class that implements it:
class Card implements PaymentStrategy {
public void pay(long amt) {
System.out.println("Card pays " + amt);
}
}
class PayPal implements PaymentStrategy {
public void pay(long amt) {
System.out.println("PayPal pays " + amt);
}
}
class Wallet implements PaymentStrategy {
public void pay(long amt) {
System.out.println("Wallet pays " + amt);
}
}
Checkout now holds a strategy and calls pay. It doesn’t care which one:
class Checkout {
PaymentStrategy strategy;
void pay(long amt) {
strategy.pay(amt);
}
}
To change the behaviour, swap the object:
Checkout checkout = new Checkout();
for (PaymentStrategy s :
List.of(new Card(), new PayPal(), new Wallet())) {
checkout.strategy = s; // swap the behaviour
checkout.pay(4900); // Checkout never changes
}
/CodeVxDev
strategy
java Strategy.java
9:17:29
That’s the whole pattern: a family of interchangeable behaviours (the strategies) behind one interface, and a context
(Checkout) that uses whichever one it’s given. Adding PayLater is now a new class. Checkout stays as it is, and
so do its tests.
You’ve already used it: Comparator
Every Comparator you pass to sort is a strategy. List.sort is the context, and the comparator decides the
order:
list.sort(byPrice);
list.sort(byRating);
record Item(String name, long price, double rating) {}
Comparator<Item> byPrice =
Comparator.comparingLong(Item::price);
Comparator<Item> byRating =
Comparator.comparingDouble(Item::rating).reversed();
/CodeVxDev
strategy
java Strategy.java
9:17:29
Same shape as Checkout: the sorting code never changes; you pass in the rule. The JDK has more: a
ThreadPoolExecutor takes a RejectedExecutionHandler (AbortPolicy, CallerRunsPolicy, DiscardPolicy) that
decides what happens when the queue is full. Since Java 8, a strategy with one method is often just a lambda.
In Spring, it’s even easier
In a Spring Boot service you don’t wire the strategies yourself. Make each one a bean and ask for a Map:
@Service
@RequiredArgsConstructor
class Checkout {
private final
Map<String, PaymentStrategy> m;
void pay(String type, long amt) {
m.get(type).pay(amt);
}
}
Spring fills the map with every PaymentStrategy bean, keyed by bean name (Lombok’s @RequiredArgsConstructor
writes the constructor it injects through). A @Component class is named after the class with a lowercase first
letter, so the keys are "card", "payPal" and "wallet".
Adding PayLater? Just one new class:
@Component
class PayLater implements
PaymentStrategy {
public void pay(long amt) { … }
}
Nothing else changes. Spring picks up the new bean and the map gains a "payLater" entry:
/CodeVxDev
strategy
java SpringWiring.java
9:17:29
That’s your interview answer: Checkout is closed for modification and open for extension, and new behaviour
arrives as a new class.
Before you ship it
A few things the short version leaves out:
-
Unknown types.
m.get(type)returnsnullfor a type nobody registered, and the next call throws aNullPointerException. Fail with a clear message instead:void pay(String type, long amt) { PaymentStrategy strategy = m.get(type); if (strategy == null) { throw new IllegalArgumentException( "Unsupported payment type: " + type); } strategy.pay(amt); } -
Stable keys. Default bean names change when someone renames a class. If the type comes from requests or a database, name the beans explicitly, for example
@Component("pay-later"). -
Order. If the strategies have to run in a set order, inject a
List<PaymentStrategy>instead; Spring sorts it by@Order.
Spot it in LLD interviews
Whenever a question has many ways to do the same job, think Strategy. The shape never changes: a context, one interface, and a class per rule.
| Question | Context | Strategies | Interface |
|---|---|---|---|
| Parking lot pricing | ParkingLot | Hourly · Flat · Weekend | PricingStrategy |
| Elevator scheduling | ElevatorController | Nearest car · SCAN · Zoned | DispatchStrategy |
| Ride fares | RideService | Standard · Surge · Shared | FareStrategy |
| Cart discounts | Cart | % off · Flat off · Buy 1 get 1 | DiscountStrategy |
| Splitting bills | Expense | Equal · Exact · Percent | SplitStrategy |
| Rate limiting | RateLimiter | Token bucket · Sliding window · Fixed window | RateLimitStrategy |
In the interview, name the interface early, give it one method, and say how a new rule gets added: one new class, and nothing else changes.
When not to use it
Strategy earns its keep when one job has several interchangeable rules and new ones keep arriving. With two cases that never change, an if-else is simpler, and an extra interface is just noise.
/CodeVxDev
strategy
cat strategy.md
9:17:29
The interview trap
An ATM also changes its behaviour: idle, card inserted, dispensing. Is that Strategy, or not?
A. Strategy   B. Not Strategy
Think about who decides when the behaviour changes. Leave A or B as a comment on the video; the next episode in this series answers it.