Behavioral

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.

Overview ยท Behavioral ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Behavioral Difficulty: Foundational Interview: Tier 1 Confused with: State
01
Section One ยท The Problem

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.

Naive approach — giant conditional in pay()
// โœ— Checkout knows the internals of every payment method class Checkout { public void pay(String method, double amount) { if (method.equals("credit_card")) { // validate card, call Stripe API, handle 3DS... System.out.println("Charged $" + amount + " to card"); } else if (method.equals("paypal")) { // redirect to PayPal, handle OAuth, confirm... System.out.println("Paid $" + amount + " via PayPal"); } else if (method.equals("crypto")) { // generate wallet address, verify blockchain tx... System.out.println("Paid $" + amount + " in crypto"); } // โ† Apple Pay? Add else-if #4. Bank transfer? #5... } }

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/else chain 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 violationCheckout knows Stripe API details, PayPal OAuth, and blockchain protocols
Without Strategy — one method with N branches
Checkout.pay() if "credit_card" else if "paypal" else if "crypto" else if "apple_pay" ← Stripe API details ← PayPal OAuth details ← blockchain details ← branches grow forever ✗ New payment = edit Checkout (OCP violation) ✗ Checkout knows internals of every provider ✗ Can’t swap at runtime or inject mocks ✗ Unbounded if/else chain

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.

02
Section Two ยท The Pattern

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.

GoF Intent: “Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — GPS navigation: You tell your GPS app “get me to the airport.” The app (context) doesn’t contain the routing logic itself — it delegates to a routing strategy. “Fastest route” takes the highway. “Shortest route” cuts through side streets. “Scenic route” follows the coast. You (the client) pick the strategy; the GPS swaps it in; the 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.

Each algorithm is encapsulated in its own class implementing a common Strategy interface
Algorithms can be developed, tested, and modified independently — Single Responsibility Principle
The Context holds a Strategy reference and delegates via strategy.execute()
Context is closed for modification — new algorithms don’t require editing the context (OCP)
The strategy can be swapped at runtime via a setter: setStrategy(new CryptoPayment())
Behaviour changes without recompilation — the client picks the algorithm; the context runs it
In modern Java, a Strategy interface with one method is a functional interface
Strategies can be lambdas: checkout.setPayment(amount -> ...) — no extra class needed for simple cases
03
Section Three ยท Anatomy

Participants & 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.
Strategy — UML Class Diagram
Checkout - strategy : PaymentStrategy + setStrategy(PaymentStrategy) + checkout(amount) «interface» PaymentStrategy + pay(double amount) : void strategy 1 CreditCardPay + pay(amount) PayPalPay + pay(amount) CryptoPay + pay(amount) Client uses creates & injects ■ Blue = Strategy interface ■ Green = concrete strategies ■ Dark = Context & Client ◇ aggregation (has-one) --▷ dashed = implements / depends Context calls strategy.pay() — never knows the concrete class
Strategy as a functional interface:
  • 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.
04
Section Four ยท How It Works

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.

Step 1 — Client creates a strategy: PaymentStrategy strategy = new CreditCardPay("4111...1111")
The concrete strategy holds its own configuration (card number, wallet address, auth token). The client picks the algorithm.
Step 2 — Client injects the strategy into the context: checkout.setStrategy(strategy)
The Checkout stores a reference to the PaymentStrategy interface — not the concrete class. It doesn’t know (or care) that it’s a credit card.
Step 3 — Context delegates: 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.
Step 4 — User switches to crypto: checkout.setStrategy(new CryptoPay("0xABC..."))
Same Checkout, same processOrder() call. But now CryptoPay.pay() runs — generates wallet address, waits for blockchain confirmation. No if/else needed.
Step 5 — Adding Apple Pay: create ApplePayStrategy implements PaymentStrategy
One new class, zero edits to Checkout. Open/Closed Principle satisfied.
Before / After — conditional vs. Strategy delegation
BEFORE — giant conditional if "card" โ†’ Stripe logic else if "paypal" โ†’ OAuth logic else if "crypto" โ†’ blockchain ✗ All logic inside Checkout ✗ Edit Checkout for every new method AFTER — Strategy delegation Checkout strategy.pay() CreditCardPay PayPalPay CryptoPay setStrategy() ✔ Checkout has zero payment logic ✔ New method = new class, zero edits to Checkout ✔ Swap at runtime
Strategy — Sequence Diagram
Client Checkout CreditCardPay setStrategy(creditCardPay) processOrder(99.99) strategy.pay(99.99) Charged $99.99 to card ****1111
The pattern in pseudocode
// โ”€โ”€ Client picks strategy, context delegates โ”€โ”€ Checkout checkout = new Checkout(); // Pay by credit card checkout.setStrategy(new CreditCardPay("4111-1111-1111-1111")); checkout.processOrder(99.99); // โ†’ Charged $99.99 to card ****1111 // Switch to crypto โ€” same context, different behaviour checkout.setStrategy(new CryptoPay("0xABC...DEF")); checkout.processOrder(49.99); // โ†’ Sent $49.99 in crypto to 0xABC...DEF // Lambda strategy for one-off discount checkout.setStrategy(amount -> System.out.println("Free! (promo applied for $" + amount + ")")); checkout.processOrder(0.00); // โ†’ Free! (promo applied for $0.00)
Strategy vs. State:
  • 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.
05
Section Five ยท Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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().
Stdlib usage — Comparator as Strategy
List<String> names = List.of("Charlie", "Alice", "Bob"); // Strategy 1: natural order names.stream().sorted(Comparator.naturalOrder()).forEach(System.out::println); // Alice, Bob, Charlie // Strategy 2: by length names.stream().sorted(Comparator.comparingInt(String::length)).forEach(System.out::println); // Bob, Alice, Charlie // Strategy 3: reverse names.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println); // Charlie, Bob, Alice
Stdlib usage — Predicate as Strategy
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6); // Filter strategy: even numbers nums.stream().filter(n -> n % 2 == 0).toList(); // [2, 4, 6] // Filter strategy: greater than 3 nums.stream().filter(n -> n > 3).toList(); // [4, 5, 6] // Same stream pipeline, different filtering algorithm each time
Every lambda in Java is a Strategy:
  • 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 whatever Comparator you provide.
  • Java 8’s functional interfaces made Strategy so lightweight that you use it dozens of times a day without realizing it.
06
Section Six ยท Implementation

Build It Once

Domain: Payment Processing. A PaymentStrategy interface with 3 concrete implementations (CreditCard, PayPal, Crypto), a Checkout context, and runtime strategy swapping.

Java — Strategy Pattern Payment Processing (core)
// โ”€โ”€ Strategy interface (functional โ€” single method) โ”€โ”€ @FunctionalInterface interface PaymentStrategy { void pay(double amount); } // โ”€โ”€ Context โ”€โ”€ class Checkout { private PaymentStrategy strategy; public void setStrategy(PaymentStrategy s) { this.strategy = s; } public void processOrder(double amount) { if (strategy == null) throw new IllegalStateException("No payment strategy set"); System.out.printf("Processing $%.2f...%n", amount); strategy.pay(amount); } }
Constructor injection vs. setter injection:
  • 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.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Leaving the conditional inside the context: The most common misapplication is extracting strategy classes but still keeping an 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.
✗ Wrong — context still selects strategy
// โœ— The if/else is still here โ€” just in a different place class Checkout { public void processOrder(String type, double amount) { PaymentStrategy s; if (type.equals("card")) s = new CreditCardPay(...); else if (type.equals("paypal")) s = new PayPalPay(...); else s = new CryptoPay(...); s.pay(amount); // โ† pattern is applied, but OCP is still violated } }
✔ Correct — client injects, context only delegates
// โœ“ Context has zero knowledge of concrete strategies class Checkout { private PaymentStrategy strategy; public void setStrategy(PaymentStrategy s) { this.strategy = s; } public void processOrder(double amount) { strategy.pay(amount); } } // Client (UI controller, config, DI container) picks the strategy
Mistake #2 — Creating a class for every trivial variation:
  • Not every strategy needs its own file.
  • In Java 8+, if the strategy is a single stateless operation, use a lambda.
  • Creating UpperCaseStrategy, LowerCaseStrategy, TrimStrategy as full classes is ceremony overkill.
  • Use: setTransform(String::toUpperCase).
Mistake #3 — Confusing Strategy with State:
  • 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.
Mistake #4 — Forgetting null-safety:
  • If the context uses setter injection and processOrder() is called before setStrategy(), you get a NullPointerException.
  • 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").
08
Section Eight ยท Decision Guide

When To Use Strategy

Use Strategy When
  • 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/else or switch that 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)
Avoid Strategy When
  • There are only 2 simple branches — a plain if/else is 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)
Strategy vs. Confused Patterns
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
Decision Flowchart
Multiple ways to do the same thing? No Direct impl Yes Who swaps the algorithm? Object itself State Client Need undo/redo or queuing? Yes Command No Strategy
09
Section Nine ยท Practice

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.
Interview Tip:
  • 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 Comparator is a Strategy.
  • Stand-out answers mention: @FunctionalInterface and lambdas for simple strategies, the Strategy vs.
  • State distinction (client swaps vs. object transitions), and the Strategy vs.
  • Template Method tradeoff (composition vs. inheritance).