Strategy Pattern
Learn to pull a varying algorithm out of an if/else chain into a family of interchangeable classes, so a new behavior can be added as one new class with zero changes to existing, tested code.
Learning objectives
- Explain why a long if/else chain over a type string is an Open/Closed Principle violation
- Extract varying algorithm logic into a shared interface and interchangeable implementations
- Swap an active strategy on a context object at runtime
- Recognize when stateless strategies can be safely shared, and when they cannot
- Identify where Strategy shows up in Java's Comparator and java.util.concurrent APIs
◆ Story
A GPS app doesn't rebuild your car every time you want a different route. Ask for the fastest route, the shortest one, or one that avoids tolls, and it swaps in a different routing algorithm while the car and the destination stay exactly the same. The car is fixed; the strategy for getting to the destination is what's interchangeable.
That's the whole idea behind this pattern: pull a piece of behavior that varies — an algorithm — out of the class that uses it, and make it swappable at runtime without touching that class's code.
A payment service is a clean, concrete way to build this from scratch. It needs to process a payment through credit card, debit card, or UPI today, with more methods realistically arriving later, the same way payment methods keep arriving in any real system over time. Building this the most obvious way first — and watching exactly why it stops being maintainable — is what makes the fix feel earned rather than arbitrary.
This pattern has a narrower, more specific job than Observer's broadcast-to-many-listeners problem: here there's exactly one active behavior at any given moment, chosen deliberately, not a list of parties all reacting simultaneously to the same event. That narrower scope is what makes Strategy one of the most directly reusable patterns in everyday code — it shows up any time "how" something gets done needs to vary independently of "that" it gets done.
The obvious first version: one method, one string parameter identifying the payment method, and an if/else chain branching on it. Each branch prints (or, in a real system, executes) the logic for that specific payment method.
This runs fine for the methods it already knows about. The real test comes with the next feature request: add support for a new payment method. Making that change means editing processPayment() directly, and today each branch is a single line, but real payment logic can genuinely be a large algorithm each — validation, external gateway calls, retries, logging — not just a println.
This is precisely the Open/Closed Principle violation from a different angle, applied specifically to swappable algorithms rather than object types. PaymentService currently has two unrelated responsibilities welded into one method: first, deciding which payment method applies, and second, actually carrying out each one's specific logic. Every new payment method reopens code that was already tested and already shipped, purely to add one more branch. And the branching logic only gets harder to maintain as it grows — a longer if/else chain, with no natural ceiling, and no way to test one method's logic independently of all the others sitting in the same method.
💻 Code example
class PaymentService { void processPayment(String paymentMethod) { if (paymentMethod.equals("credit_card")) { System.out.println("Making payment via credit card"); } else if (paymentMethod.equals("debit_card")) { System.out.println("Making payment via debit card"); } else { throw new IllegalArgumentException("Unsupported payment method"); } } } // Adding UPI means editing this method directly, growing an if/else chain // with no ceiling and no way to test one method's logic in isolation.
Reason through what PaymentService should actually hold, rather than what it currently does. The logic for each payment method is really a self-contained algorithm — "given an amount, process it this specific way" — that has nothing to do with which method the user happened to pick. Each algorithm deserves its own class, sharing one interface, so PaymentService can call any of them identically, without an if/else in sight. PaymentService itself should just hold a reference to whichever one is currently selected, and delegate to it.
This is the Strategy pattern: pull the varying algorithm out into its own family of interchangeable classes, all implementing a shared interface, and have the context class hold a reference to whichever one is currently active. This is composition doing the job that would otherwise fall to inheritance or branching logic — favoring "has-a swappable behavior" over "is-a fixed type."
Start with the shared contract: one method, processPayment(double amount), that every strategy must implement. Each old if/else branch becomes its own class implementing that interface — CreditCardPayment, DebitCardPayment, each holding exactly the logic that used to live in one branch.
PaymentService — now called the context — holds a single PaymentStrategy field, set explicitly through a setPaymentStrategy() method, and a pay() method that does nothing but delegate to whatever strategy is currently set. pay() has no idea which concrete strategy it's holding, and it doesn't need to — this is the same generic-delegation shape used to fix the weather station's notification loop, applied here to a single active choice instead of a whole list of listeners.
💻 Code example
interface PaymentStrategy { void processPayment(double amount); // every strategy must be able to do exactly this } class CreditCardPayment implements PaymentStrategy { public void processPayment(double amount) { System.out.println("Making payment via credit card: " + amount); } } class DebitCardPayment implements PaymentStrategy { public void processPayment(double amount) { System.out.println("Making payment via debit card: " + amount); } } class PaymentService { private PaymentStrategy strategy; // holds ONE currently-active strategy, swappable at runtime void setPaymentStrategy(PaymentStrategy strategy) { // chosen explicitly by the caller this.strategy = strategy; } void pay(double amount) { strategy.processPayment(amount); // delegates — never needs to change again } }
Wire it together, then prove the fix by adding a completely new payment method and checking exactly what has to change.
Set a CreditCardPayment strategy, call pay(250.0), and it prints the credit card message. Swap the strategy at runtime with setPaymentStrategy(new DebitCardPayment()) on the exact same PaymentService object, call pay(100.0) again, and it now prints the debit card message — no new PaymentService instance was needed, just a different object plugged into the same field.
Now add UPI support. The entire diff required is one new class, UpiPayment, implementing PaymentStrategy with its own processPayment() body. PaymentService.pay() and setPaymentStrategy() are not touched at all — zero lines change in a class that was already written, tested, and shipped. That's the exact proof point the Open/Closed Principle asks for: open for extension (a new strategy class), closed for modification (the context class never changes). Strategy is, structurally, that principle applied specifically to swappable behavior instead of swappable object types.
It's worth contrasting this with the original if/else version one more time. There, adding UPI meant opening processPayment(), finding the right spot in the chain, and inserting a new branch — a change to code that every other payment method's logic also lived inside, with real risk of a typo breaking an unrelated branch. Here, UpiPayment is written, compiled, and tested in complete isolation from CreditCardPayment and DebitCardPayment, and plugging it in is a one-line call from wherever the application wires things together.
💻 Code example
class UpiPayment implements PaymentStrategy { // one new class — PaymentService is not touched public void processPayment(double amount) { System.out.println("Making payment via UPI: " + amount); } } public class Main { public static void main(String[] args) { PaymentService service = new PaymentService(); service.setPaymentStrategy(new CreditCardPayment()); service.pay(250.0); // Making payment via credit card: 250.0 service.setPaymentStrategy(new DebitCardPayment()); // swapped at RUNTIME service.pay(100.0); // Making payment via debit card: 100.0 service.setPaymentStrategy(new UpiPayment()); service.pay(120.0); // Making payment via UPI: 120.0 } }
▲ Edge case — calling pay() before a strategy is set
strategy defaults to null until setPaymentStrategy() is called, so calling pay() before that throws a NullPointerException with a message that doesn't explain the real problem. A production version would either require a strategy through the constructor, making the invalid state unrepresentable, or guard pay() with a clear "no payment method selected" error instead of letting a raw NPE leak out to the caller.
▲ Edge case — stateless strategies can safely be shared and reused
None of the strategy classes here hold any fields of their own — CreditCardPayment doesn't remember anything between calls. That means a single CreditCardPayment instance can safely be reused across many PaymentService objects at once, even from multiple threads simultaneously, since there's no shared mutable state that concurrent use could corrupt. The moment a strategy needs to hold configuration — an API key, a fee percentage — that assumption needs to be rechecked deliberately, since a stateful strategy shared across threads can introduce real race conditions.
▲ Trade-off — Strategy can be overkill for a genuinely fixed set of two options
If a piece of behavior only ever has two stable variants that will never realistically grow to a third, introducing a full interface plus two implementing classes is more ceremony than the problem needs — a simple boolean flag or a two-way if/else may be perfectly appropriate. Strategy earns its complexity when the number of variants is expected to grow, or when each variant's logic is substantial enough to deserve isolated testing.
◆ Where this pattern actually shows up
java.util.Comparator— passed intoList.sort(Comparator)orCollections.sort(), this is a textbook Strategy: the sorting algorithm itself stays fixed, but the comparison logic deciding element order is fully swappable and supplied by the caller.ThreadPoolExecutor's rejection policies —RejectedExecutionHandlerimplementations likeAbortPolicy,CallerRunsPolicy, andDiscardPolicyare interchangeable strategies for what happens when a thread pool's queue is full, all satisfying the same one-method interface.- Compression and encoding libraries — choosing gzip versus zip versus a different codec is a strategy choice behind a shared compression interface.
- Route planners — exactly Section one's opening story: fastest, shortest, or avoid-tolls, all interchangeable behind the same "compute a route" contract.
- Validation frameworks — swappable validation rules applied to the same input, each rule satisfying a shared validator interface.
- Dependency injection frameworks — swapping a real implementation for a mock or test double behind the same interface, at configuration time rather than at runtime, is Strategy applied to testing.
Q: What does the Strategy pattern actually extract into its own interface? : The varying algorithm itself — pulled out of the context class into a family of interchangeable classes sharing one interface, so the context can delegate to any of them identically without branching logic.
Q: How is Strategy specifically a form of the Open/Closed Principle? : Adding a new algorithm means writing one new class, with zero changes to the already-tested context class — exactly "open for extension, closed for modification," applied to swappable behavior instead of object creation.
Q: Why can a single stateless strategy instance safely be reused across many context objects, even across threads? : Because it holds no fields of its own — there's no shared mutable state for concurrent use to corrupt. A stateful strategy shared the same way would need explicit thread-safety consideration.
Q: What happens if pay() is called before setPaymentStrategy(), and how would you improve on that? : strategy is null, so a NullPointerException leaks out with an unhelpful message. A better design requires a strategy at construction time, or throws a clear, specific error explaining that no payment method was selected.
Q: When might introducing the Strategy pattern be overkill? : When behavior only ever has two genuinely fixed variants with no realistic growth — a boolean flag or simple conditional is often clearer than a full interface plus multiple implementing classes for that case.
Want a visual for this concept?
Generate a diagram tailored to “Strategy Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →