Design Patterns in Java

Strategy Pattern

15 min Lesson 5 of 13

Strategy Pattern

The Strategy pattern is one of the most frequently used behavioural patterns in professional Java. Its core idea is simple: define a family of algorithms, encapsulate each one behind a common interface, and make them interchangeable at runtime — without modifying the clients that use them. The pattern directly addresses the Open/Closed Principle: code is open for extension (add a new algorithm) but closed for modification (existing code does not change).

The Problem Without Strategy

Imagine a payment module that supports credit cards, PayPal and bank transfers. Without Strategy, the typical first draft puts all the logic in one class with a chain of if/else or a switch:

// Anti-pattern: switch-based dispatch public class PaymentProcessor { public void pay(String method, double amount) { if (method.equals("CARD")) { // card-specific logic } else if (method.equals("PAYPAL")) { // PayPal-specific logic } else if (method.equals("BANK")) { // bank-transfer logic } // every new payment type requires editing THIS class } }

Every new payment method forces a change inside PaymentProcessor, it is impossible to test algorithms in isolation, and the class grows without bound. Strategy solves all three problems.

Structure of the Pattern

Three roles make up the pattern:

  • Strategy interface — the contract every algorithm must satisfy.
  • Concrete strategies — one class per algorithm, each implementing the interface.
  • Context — holds a reference to a strategy, delegates work to it, and can swap strategies at runtime.
Key insight: the Context knows that it has a strategy but has no idea which one. The calling code makes that decision and injects it — a clean example of programming to an interface rather than an implementation.

Canonical Java Example — Payment Processing

// Strategy interface @FunctionalInterface public interface PaymentStrategy { void pay(double amount); } // Concrete strategies public class CreditCardPayment implements PaymentStrategy { private final String cardNumber; public CreditCardPayment(String cardNumber) { this.cardNumber = cardNumber; } @Override public void pay(double amount) { System.out.printf("Charged $%.2f to card ending %s%n", amount, cardNumber.substring(cardNumber.length() - 4)); } } public class PayPalPayment implements PaymentStrategy { private final String email; public PayPalPayment(String email) { this.email = email; } @Override public void pay(double amount) { System.out.printf("Sent $%.2f via PayPal to %s%n", amount, email); } } // Context public class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } // Swap strategies at runtime public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; } public void checkout(double amount) { strategy.pay(amount); } }

Usage is expressive and does not involve any conditional branching inside PaymentContext:

PaymentContext ctx = new PaymentContext(new CreditCardPayment("4111111111111234")); ctx.checkout(99.99); // Charged $99.99 to card ending 1234 ctx.setStrategy(new PayPalPayment("user@example.com")); ctx.checkout(49.00); // Sent $49.00 via PayPal to user@example.com

Lambdas as Strategies (Java 8+)

Notice the @FunctionalInterface annotation on PaymentStrategy — it has exactly one abstract method. That makes any lambda expression a valid concrete strategy, eliminating the need for a separate class for simple algorithms:

// Anonymous class — verbose, older style PaymentStrategy cryptoOld = new PaymentStrategy() { @Override public void pay(double amount) { System.out.printf("Paying $%.2f in crypto%n", amount); } }; // Lambda — clean, modern Java PaymentStrategy crypto = amount -> System.out.printf("Paying $%.2f in crypto%n", amount); ctx.setStrategy(crypto); ctx.checkout(200.00);

Method references work equally well when a pre-existing method already has the right signature:

public class PaymentLogger { public static void logPayment(double amount) { System.out.println("Audit: $" + amount); } } // Method reference as a strategy ctx.setStrategy(PaymentLogger::logPayment); ctx.checkout(150.0);
When to reach for lambdas vs. full classes: use a lambda when the algorithm is a single coherent expression or a few lines with no internal state. Create a named class when the strategy holds injected dependencies (like a cardNumber), owns mutable state, or needs to be unit-tested independently by name.

Real-World Example — Sorting with Comparator

You already use Strategy every day. java.util.Comparator is a Strategy interface. Collections.sort() and List.sort() are the Context:

record Employee(String name, int salary) {} List<Employee> team = List.of( new Employee("Zara", 95_000), new Employee("Alice", 80_000), new Employee("Bob", 110_000) ); // Strategy 1: sort by name List<Employee> byName = team.stream() .sorted(Comparator.comparing(Employee::name)) .toList(); // Strategy 2: sort by salary descending List<Employee> bySalaryDesc = team.stream() .sorted(Comparator.comparingInt(Employee::salary).reversed()) .toList();

The stream pipeline (the Context) is unchanged; only the Comparator (the Strategy) differs. The JDK designers applied the pattern so you never need to modify List to add a new sort order.

Strategy vs. Similar Patterns

  • Template Method — fixes the skeleton in a base class and lets subclasses fill in steps via inheritance. Strategy delegates the whole algorithm via composition. Prefer composition over inheritance.
  • State — looks identical in code (interface + context with a reference), but State transitions itself; Strategy is swapped by the caller. If the object decides which strategy to use next, it is State.
  • Command — encapsulates a request as an object, often supporting undo. Strategy encapsulates an algorithm; the two are often combined (a Command chooses among Strategies).

Trade-offs and When Not to Use It

The pattern is powerful but not cost-free:

  • Clients must be aware of strategies. The caller needs to know which strategy to inject. If selection logic is complex, move it to a factory or a configuration layer so the client stays clean.
  • Overkill for two options. If a boolean flag between two simple branches is stable, a Strategy hierarchy adds indirection without benefit. Apply it when you anticipate a growing family of algorithms or when you need runtime swapping.
  • Interface explosion. Each distinct algorithm family needs its own interface. Keep granularity appropriate — one interface per axis of variation.
Do not reach for Strategy as a reflex. First ask whether the algorithms are genuinely interchangeable and whether you actually need runtime switching. If the answer is yes to both, the pattern earns its place. If not, a simple helper method or a plain switch expression may be clearer.

Summary

The Strategy pattern encapsulates interchangeable algorithms behind a shared interface, letting you swap behaviour at runtime without modifying the Context. In modern Java, @FunctionalInterface turns any Strategy into a target for lambdas and method references, reducing boilerplate dramatically. You see it throughout the JDK — Comparator, Predicate, Runnable, and ExecutorService are all Strategy in action. Apply it when you have a family of algorithms that varies independently from the code that uses them.