Define a family of algorithms, encapsulate each one, and make them interchangeable — let the algorithm vary independently from clients that use it.
Strategy defines a family of algorithms, puts each in a separate class, and makes their objects interchangeable. The Context holds a reference to a Strategy and delegates the algorithm to it. The client chooses which strategy to use at runtime.
This replaces conditional logic that selects a behaviour with polymorphism — each branch of the if-else becomes its own Strategy class.
buildRoute(A, B) on whichever strategy is selected.You have an e-commerce checkout that sorts products by price, rating, or name depending on user selection. With if-else: if (sort == "price") { ... } else if (sort == "rating") { ... }. Adding a new sort type means modifying the checkout class — violating Open/Closed.
Strategy makes each sort algorithm a class: PriceSortStrategy, RatingSortStrategy, NameSortStrategy. The checkout just calls strategy.sort(products) — adding a new sort is adding one new class.
// ── EXAMPLE 1: Sort strategies ─────────────────────────────────────── // Strategy interface public interface SortStrategy<T extends Comparable<T>> { void sort(List<T> data); } // Concrete Strategies public class BubbleSortStrategy<T extends Comparable<T>> implements SortStrategy<T> { public void sort(List<T> data) { System.out.println("Bubble sorting " + data.size() + " items"); // O(n²) bubble sort implementation for (int i = 0; i < data.size()-1; i++) for (int j = 0; j < data.size()-i-1; j++) if (data.get(j).compareTo(data.get(j+1)) > 0) Collections.swap(data, j, j+1); } } public class QuickSortStrategy<T extends Comparable<T>> implements SortStrategy<T> { public void sort(List<T> data) { System.out.println("Quick sorting " + data.size() + " items"); data.sort(Comparator.naturalOrder()); // simplified } } // Context public class Sorter<T extends Comparable<T>> { private SortStrategy<T> strategy; public Sorter(SortStrategy<T> strategy) { this.strategy = strategy; } public void setStrategy(SortStrategy<T> strategy) { this.strategy = strategy; } public void sort(List<T> data) { strategy.sort(data); } } // ── EXAMPLE 2: Payment strategies (real-world) ────────────────────── public interface PaymentStrategy { boolean pay(double amount); } public class CreditCardStrategy implements PaymentStrategy { private final String cardNumber, cvv; public CreditCardStrategy(String card, String cvv) { cardNumber=card; this.cvv=cvv; } public boolean pay(double amount) { System.out.println("Charging $"+amount+" to card ****"+cardNumber.substring(12)); return true; } } public class PayPalStrategy implements PaymentStrategy { private final String email; public PayPalStrategy(String email) { this.email = email; } public boolean pay(double amount) { System.out.println("PayPal payment $"+amount+" from "+email); return true; } } public class ShoppingCart { private PaymentStrategy paymentStrategy; private double total; public void setPaymentStrategy(PaymentStrategy p) { paymentStrategy = p; } public void addItem(double price) { total += price; } public void checkout() { if (paymentStrategy == null) throw new IllegalStateException("No payment strategy set"); boolean success = paymentStrategy.pay(total); if (success) total = 0; } } // ── Modern Java: lambdas replace simple strategy classes ───────────── SortStrategy<Integer> reverseSort = data -> data.sort(Comparator.reverseOrder()); SortStrategy<Integer> naturalSort = data -> data.sort(Comparator.naturalOrder()); Sorter<Integer> sorter = new Sorter<>(naturalSort); sorter.sort(Arrays.asList(5, 3, 1, 4)); sorter.setStrategy(reverseSort); // swap at runtime sorter.sort(Arrays.asList(5, 3, 1, 4));
java.util.Comparator — textbook Strategy; pass different comparators to Collections.sort()AuthenticationStrategy — different auth algorithms plugged inResourceLoader — different strategies for classpath, file system, URL resourcesObjectMapper — configurable serialisation/deserialisation strategiesexecute() throws NullPointerException with no useful error messageif (strategy instanceof QuickSort) to add special-case logic — defeats the point of the pattern entirelyIllegalStateException("No strategy configured") with a clear message in execute() before calling the strategyexecute(data) receives everything it needs as method parameters — no callbacks into Context, no circular dependencyexecute() — no instance fields that persist between calls. Stateless strategies are safe to share// ❌ BREAKING — null strategy causes NPE public class Sorter { private SortStrategy strategy; // never set! public void sort(List data) { strategy.sort(data); // ❌ NPE — no error message } } // ✅ FIX 1 — default strategy in constructor public Sorter() { this.strategy = new QuickSortStrategy(); // ✅ always has a valid strategy } // ✅ FIX 2 — fail fast with meaningful message public void sort(List data) { if (strategy == null) throw new IllegalStateException("Sort strategy not configured"); strategy.sort(data); } // ❌ BREAKING — stateful shared strategy public class StatefulQuickSort implements SortStrategy { private int comparisons = 0; // ❌ state shared between concurrent calls public void sort(List data) { comparisons++; // Thread A and B both increment — race condition } } // ✅ FIX — stateless strategy, metrics in method scope public class StatelessQuickSort implements SortStrategy { public void sort(List data) { int comparisons = 0; // ✅ local variable — thread-safe // ... sort logic ... System.out.println("Comparisons: " + comparisons); } } // ❌ BREAKING — strategy callbacks into context public interface SortStrategy { void sort(Sorter context); // ❌ strategy imports Context — circular dependency } // ✅ FIX — pass data directly, no context reference public interface SortStrategy<T> { void sort(List<T> data); // ✅ pure data in, no context needed }
strategy: Strategy, calls strategy.execute(data). Modern Java: use lambdas/method refs for stateless simple strategies.