Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. A foundational behavioral pattern — the cure for giant conditionals, the backbone of pluggable policies.
Why Strategy Exists
You are building an e-commerce checkout. Orders can be paid by credit card, PayPal, or cryptocurrency. The naive approach: a giant if/else or switch inside the pay() method that branches on the payment type. Every time a new payment method is added (Apple Pay, bank transfer, buy-now-pay-later), you crack open the Checkout class and wedge in another branch.
What goes wrong:
- Open/Closed violation — every new payment method means editing
Checkout.pay(); the class is never closed for modification - Giant conditional — the
if/elsechain grows unbounded; each branch has completely different logic - No runtime swapping — the payment method is a string checked at each call; you can’t inject a mock for testing or swap providers dynamically
- Single Responsibility violation —
Checkoutknows Stripe API details, PayPal OAuth, and blockchain protocols
This is the problem Strategy solves — extract each payment algorithm into its own class behind a common interface. The Checkout holds a reference to the interface and delegates pay() to whatever strategy is plugged in. Adding Apple Pay means creating one new class — zero edits to Checkout.
What Is Strategy?
Strategy is a behavioral pattern that defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable. The context (e.g. Checkout) holds a reference to a strategy interface and delegates the varying behaviour to it. The strategy can be swapped at runtime — the context doesn’t know or care which concrete algorithm is running.
— Gamma, Helm, Johnson, Vlissides (1994)
navigate() method doesn’t change. Adding a “toll-free route” means plugging in a new strategy — the GPS app itself is untouched.
Key insight: Strategy replaces conditional branching with polymorphism. Instead of if (type == X) doX(); else if (type == Y) doY();, you write strategy.execute() — and the runtime type of strategy determines the behaviour. This is the fundamental OOP move: replace conditionals with objects. It’s also why Strategy is the #1 interview pattern — it directly tests Open/Closed Principle, composition over inheritance, and runtime polymorphism.
Strategy interfaceStrategy reference and delegates via strategy.execute()setStrategy(new CryptoPayment())checkout.setPayment(amount -> ...) — no extra class needed for simple casesParticipants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| Strategy | An interface declaring the algorithm method (e.g. pay(amount)). Often a functional interface in modern Java (single abstract method). | The concept of “a routing algorithm” — any way to get from A to B. |
| Concrete Strategy | A class implementing the Strategy interface with a specific algorithm. Each variant (CreditCard, PayPal, Crypto) is its own class. | Fastest Route, Shortest Route, Scenic Route — each knows how to compute its own path. |
| Context | Holds a reference to a Strategy and delegates the varying behaviour to it via strategy.execute(). Strategy can be set via constructor or setter. | The GPS app — it calls the routing strategy but doesn’t know the road details itself. |
| Client | Creates a concrete strategy and passes it to the context. The client decides which algorithm to use; the context decides when to call it. | You, the driver — picking “Fastest Route” in the GPS settings. |
- In Java 8+, a Strategy interface with a single method (e.g.
pay(double)) is a @FunctionalInterface.- This means you can pass a lambda instead of creating a class:
checkout.setStrategy(amount -> System.out.println("Paid " + amount)). - Use full classes when the strategy has state or configuration; use lambdas for simple, stateless transformations.
The Pattern In Motion
Scenario: An e-commerce checkout processes a $99.99 order. The user selects a payment method; the system plugs in the matching strategy and delegates.
PaymentStrategy strategy = new CreditCardPay("4111...1111")checkout.setStrategy(strategy)Checkout stores a reference to the PaymentStrategy interface — not the concrete class. It doesn’t know (or care) that it’s a credit card.checkout.processOrder(99.99) calls strategy.pay(99.99)CreditCardPay.pay() validates the card, calls Stripe, charges $99.99. The checkout method has zero payment logic inside it.checkout.setStrategy(new CryptoPay("0xABC..."))Checkout, same processOrder() call. But now CryptoPay.pay() runs — generates wallet address, waits for blockchain confirmation. No if/else needed.ApplePayStrategy implements PaymentStrategyCheckout. Open/Closed Principle satisfied.- Both use the same UML structure (context delegates to an interface).
- The difference: in Strategy, the client swaps the algorithm from outside.
- In State, the object itself transitions to a new state from inside based on internal logic.
- Strategy = client-driven swap. State = self-driven transition.
You Already Use This
Strategy is everywhere in the JDK. Any time you pass a “how to do X” object to a method that decides “when to do X,” you’re using Strategy — even if the JDK doesn’t call it that.
java.util.Comparator — the textbook Strategy. Collections.sort(list, comparator) is the context; the Comparator is the strategy. You swap the sorting algorithm (by price, by name, by date) without changing the sort call. Java 8 made it a @FunctionalInterface, so you can pass Comparator.comparing(Order::getPrice) as a lambda. java.util.function.* — Predicate, Function, Consumer, Supplier are all single-method functional interfaces, i.e. strategy contracts. stream.filter(predicate) delegates filtering logic to whatever Predicate you plug in. javax.swing.LayoutManager — Swing containers delegate how to position components to a LayoutManager strategy (FlowLayout, BorderLayout, GridBagLayout). Same container, different layout algorithm, swapped via setLayout(). - When you write
list.sort((a, b) -> a.length() - b.length()), you’re passing a Strategy. - The
sort()method doesn’t know the comparison logic — it delegates to whateverComparatoryou provide. - Java 8’s functional interfaces made Strategy so lightweight that you use it dozens of times a day without realizing it.
Build It Once
Domain: Payment Processing. A PaymentStrategy interface with 3 concrete implementations (CreditCard, PayPal, Crypto), a Checkout context, and runtime strategy swapping.
- If the strategy is always needed and doesn’t change, inject it via the constructor:
new Checkout(strategy). - If it may change at runtime (user picks payment method), use a setter.
- Constructor injection is safer (no null state); setter injection is more flexible.
- Both are valid — pick based on whether the strategy is fixed or swappable.
Common Mistakes
if/else in the context to pick which strategy to use. The context should never select the concrete strategy — that’s the client’s job. If the context has if (type == "card") setStrategy(new CreditCardPay()), you’ve just moved the conditional, not eliminated it.
- Not every strategy needs its own file.
- In Java 8+, if the strategy is a single stateless operation, use a lambda.
- Creating
UpperCaseStrategy,LowerCaseStrategy,TrimStrategyas full classes is ceremony overkill. - Use:
setTransform(String::toUpperCase).
- Both patterns have identical UML structure (context delegates to an interface).
- The key difference: in Strategy, the client swaps the algorithm from outside.
- In State, the object itself changes its behaviour based on internal state transitions.
- If the object calls
setState()on itself inside its methods, that’s State, not Strategy.
- If the context uses setter injection and
processOrder()is called beforesetStrategy(), you get aNullPointerException. - Fix: Either require the strategy in the constructor, or add a null-check with a meaningful error:
if (strategy == null) throw new IllegalStateException("No payment strategy set").
When To Use Strategy
- You have multiple algorithms for the same task and want to swap them at runtime (payment, sorting, routing, compression, validation)
- A method contains a giant
if/elseorswitchthat selects behaviour based on a type flag — extract each branch into a strategy class - You want to follow Open/Closed Principle — add new algorithms without modifying existing code
- You need to test algorithms independently — each strategy can be unit-tested in isolation without the context
- You want to use lambdas for simple, one-off algorithms (Java’s functional interfaces are strategies)
- There are only 2 simple branches — a plain
if/elseis more readable than the Strategy ceremony - The algorithm never changes — if there’s always exactly one implementation, Strategy adds indirection for no benefit
- The behaviour changes based on internal state — use State pattern instead (object transitions itself)
- You need to undo/redo or queue operations — use Command (which encapsulates the entire request, not just the algorithm)
| Pattern | Who swaps? | Focus | When to pick it |
|---|---|---|---|
| Strategy ← this | Client (from outside) | Swap the algorithm for a task | Multiple interchangeable algorithms, client picks |
| State | Object itself (from inside) | Change behaviour when internal state changes | Finite state machine, object transitions itself |
| Command | Invoker triggers | Encapsulate a request as an object | Undo/redo, queuing, logging actions |
| Template Method | Subclass overrides steps | Define skeleton, subclass fills steps | Fixed algorithm structure, varying steps via inheritance |
Problems To Solve
Strategy problems test whether you can eliminate conditionals with polymorphism, inject algorithms at runtime, and distinguish Strategy from State and Command.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | Text Formatter Build a TextEditor context that formats text using pluggable strategies: UpperCase, LowerCase, TitleCase, SlugCase. The user picks the format; the editor applies it. Add a 5th format (CamelCase) without editing the editor. | Tests the basic pattern: interface with format(String), 4 concrete classes, setter injection. Since each strategy is stateless and single-method, also implement them as lambdas and String::toUpperCase method references. This shows how Strategy and Java 8 functional interfaces merge. |
| Medium | Shipping Cost Calculator An e-commerce order has a weight and destination. Build strategies for StandardShipping ($5 + $0.50/kg), ExpressShipping ($15 + $1.00/kg), and FreeShipping (over $100 orders). The Order context picks the shipping strategy based on user selection and calculates the total. | Tests strategies with state (rate per kg, base cost) and strategy selection happening externally. The trap: don’t put the selection logic inside Order. The controller/UI picks the strategy and injects it. Bonus: add a PickupStrategy (cost = $0) without touching Order. |
| Medium | Compression Strategy Build a file archiver that supports multiple compression algorithms: ZipCompression, GzipCompression, NoCompression. Each strategy implements compress(byte[]) and decompress(byte[]). The archiver delegates compression to the plugged-in strategy. | Tests a Strategy interface with two methods (compress + decompress) — not a functional interface, so lambdas don’t work here. This forces full class implementations. Also tests that the archiver is decoupled: switching from Zip to Gzip requires changing only the injected strategy, not the archiver code. |
| Hard | Dynamic Discount Engine Build a pricing engine where discount rules are strategies: PercentageDiscount(10%), FlatDiscount($20), BuyOneGetOneFree, LoyaltyDiscount(tier). Multiple strategies can be composed (stacked): apply percentage first, then flat, then loyalty. Order of composition matters. | Tests strategy composition — a CompositeDiscount that holds a List<DiscountStrategy> and applies them in sequence (Decorator-like chaining within Strategy). The order changes the final price. This combines Strategy with Composite and tests whether candidates understand that strategies can be composed, not just swapped. |
- When asked about Strategy, the interviewer wants to see: (1) a Strategy interface with a single algorithm method; (2) concrete strategy classes โ not Foo/Bar, use a real domain; (3) a Context that delegates via the interface โ zero conditionals; (4) runtime swapping via setter or constructor; (5) awareness that
Comparatoris a Strategy. - Stand-out answers mention:
@FunctionalInterfaceand lambdas for simple strategies, the Strategy vs. - State distinction (client swaps vs. object transitions), and the Strategy vs.
- Template Method tradeoff (composition vs. inheritance).