Strategy Pattern
Category: Behavioral
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
Overviewโ
The Strategy pattern extracts related algorithms into separate classes (strategies) and lets the client switch between them at runtime. Instead of hardcoding a specific algorithm, you program to an interface and inject the desired strategy.
Key characteristics:
- Defines a common interface for a family of algorithms
- Each algorithm is encapsulated in its own class
- Client delegates work to the strategy object instead of implementing it directly
- Strategies can be swapped at runtime
โ Problem & Solutionโ
The Problem: One day you decided to create a navigation app for casual travelers. The app was centered around a beautiful map which helped users quickly orient themselves in any city. One of the most requested features for the app was automatic route planning. Once a user entered an address, they should be able to see the fastest route to that destination displayed on the map. The first version of the app could only build routes over roads. People who traveled by car were bursting with joy. But apparently, not everybody likes to drive on their vacation. So with the next update, you added an option to build walking routes. Right after that, you added another option to let people use public transport in their routes. However, that was only the beginning. Later you planned to add route building for cyclists. And even later, another option for building routes through all of a city's tourist attractions. While from a business perspective the app was a success, the technical part caused you many headaches. Each time you added a new routing algorithm, the main class of the navigator doubled in size. At some point, the beast became too hard to maintain.
The Solution: The Strategy pattern suggests that you take a class that does something specific in a lot of different ways and extract all of these algorithms into separate classes called strategies. The original class, called context, must have a field for storing a reference to one of the strategies. The context delegates the work to a linked strategy object instead of executing it on its own. The context isn't responsible for selecting an appropriate algorithm for the job. Instead, the client passes the desired strategy to the context. In fact, the context doesn't know much about strategies. It works with all strategies through the same generic interface, which only exposes a single method for triggering the algorithm encapsulated within the selected strategy.
๐ Real-World Analogyโ
Imagine that you have to get to the airport. You can catch a bus, order a cab, or get on your bicycle. These are your transportation strategies. You can pick one of the strategies depending on factors such as budget or time constraints.
๐๏ธ Structureโ
When to Useโ
- You have multiple algorithms for a specific task and want to switch between them
- You want to avoid conditional logic (
if/else,switch) that selects an algorithm - You want to isolate the algorithm implementation from the code that uses it
- Related classes differ only in their behavior
How It Worksโ
Sorting Strategy Exampleโ
// Strategy interface
public interface SortStrategy<T extends Comparable<T>> {
void sort(List<T> data);
String getName();
}
// Concrete strategies
public class QuickSortStrategy<T extends Comparable<T>> implements SortStrategy<T> {
@Override
public void sort(List<T> data) {
Collections.sort(data); // simplified โ uses Java's built-in sort
System.out.println("Sorted using QuickSort");
}
@Override
public String getName() { return "QuickSort"; }
}
public class BubbleSortStrategy<T extends Comparable<T>> implements SortStrategy<T> {
@Override
public void sort(List<T> data) {
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) {
T temp = data.get(j);
data.set(j, data.get(j + 1));
data.set(j + 1, temp);
}
}
}
System.out.println("Sorted using BubbleSort");
}
@Override
public String getName() { return "BubbleSort"; }
}
public class MergeSortStrategy<T extends Comparable<T>> implements SortStrategy<T> {
@Override
public void sort(List<T> data) {
mergeSort(data, 0, data.size() - 1);
System.out.println("Sorted using MergeSort");
}
private void mergeSort(List<T> data, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(data, left, mid);
mergeSort(data, mid + 1, right);
merge(data, left, mid, right);
}
}
private void merge(List<T> data, int left, int mid, int right) {
// merge logic omitted for brevity
}
@Override
public String getName() { return "MergeSort"; }
}
Context โ Auto-Selecting Strategyโ
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) {
System.out.println("Using strategy: " + strategy.getName());
strategy.sort(data);
}
// Auto-select based on data size
public static <T extends Comparable<T>> Sorter<T> autoSelect(List<T> data) {
if (data.size() < 10) {
return new Sorter<>(new BubbleSortStrategy<>());
} else if (data.size() < 1000) {
return new Sorter<>(new QuickSortStrategy<>());
} else {
return new Sorter<>(new MergeSortStrategy<>());
}
}
}
// Usage
List<Integer> data = new ArrayList<>(List.of(5, 2, 8, 1, 9, 3));
Sorter<Integer> sorter = Sorter.autoSelect(data);
sorter.sort(data); // BubbleSort for small list
// Switch strategy at runtime
sorter.setStrategy(new MergeSortStrategy<>());
sorter.sort(data);
Payment Processing Exampleโ
public interface PaymentStrategy {
boolean pay(BigDecimal amount);
String getPaymentMethod();
}
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
private final String cvv;
public CreditCardPayment(String cardNumber, String cvv) {
this.cardNumber = cardNumber;
this.cvv = cvv;
}
@Override
public boolean pay(BigDecimal amount) {
System.out.printf("๐ณ Paid $%s with credit card ending in %s%n",
amount, cardNumber.substring(cardNumber.length() - 4));
return true;
}
@Override
public String getPaymentMethod() { return "Credit Card"; }
}
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean pay(BigDecimal amount) {
System.out.printf("๐ง Paid $%s via PayPal (%s)%n", amount, email);
return true;
}
@Override
public String getPaymentMethod() { return "PayPal"; }
}
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public boolean pay(BigDecimal amount) {
System.out.printf("๐ช Paid $%s via crypto wallet %s%n",
amount, walletAddress.substring(0, 8) + "...");
return true;
}
@Override
public String getPaymentMethod() { return "Cryptocurrency"; }
}
// Context
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public boolean checkout() {
BigDecimal total = items.stream()
.map(Item::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return paymentStrategy.pay(total);
}
}
Strategy with Java Lambda (Functional Approach)โ
@FunctionalInterface
public interface DiscountStrategy {
BigDecimal applyDiscount(BigDecimal price);
}
public class PricingService {
public BigDecimal calculatePrice(BigDecimal basePrice, DiscountStrategy discount) {
return discount.applyDiscount(basePrice);
}
}
// Usage with lambdas โ no need for separate classes
PricingService service = new PricingService();
// 10% off
BigDecimal price1 = service.calculatePrice(new BigDecimal("100"),
price -> price.multiply(new BigDecimal("0.90")));
// Flat $15 off
BigDecimal price2 = service.calculatePrice(new BigDecimal("100"),
price -> price.subtract(new BigDecimal("15")));
// Buy above $50, get 20% off
BigDecimal price3 = service.calculatePrice(new BigDecimal("100"),
price -> price.compareTo(new BigDecimal("50")) > 0
? price.multiply(new BigDecimal("0.80")) : price);
Strategy vs. Template Methodโ
| Aspect | Strategy | Template Method |
|---|---|---|
| Mechanism | Composition (has-a) | Inheritance (is-a) |
| Algorithm structure | Entire algorithm is replaceable | Overall structure is fixed; steps vary |
| Flexibility | Swappable at runtime | Fixed at compile time |
| Coupling | Loose โ strategy is a separate object | Tight โ subclass is bound to superclass |
| Use case | Interchangeable behaviors | Variations on a common workflow |
Real-World Examplesโ
| Framework/Library | Description |
|---|---|
java.util.Comparator | Sort strategy โ Collections.sort(list, comparator) |
java.util.concurrent.RejectedExecutionHandler | Strategy for handling tasks rejected by a ThreadPoolExecutor |
Spring Resource | ClassPathResource, FileSystemResource, UrlResource โ different loading strategies |
javax.validation | Different validation strategies per annotation |
java.util.zip | Deflater/Inflater compression strategies |
Advantages & Disadvantagesโ
| Advantages | Disadvantages |
|---|---|
| Eliminates conditional algorithm selection | Clients must be aware of different strategies |
| Easy to extend with new algorithms (OCP) | Increased number of objects |
| Strategies are reusable across contexts | Overkill for only two variants |
| Runtime algorithm switching | Communication overhead between context and strategy |
Interview Questionsโ
Q1: What is the Strategy pattern and how does it differ from using if/else?
The Strategy pattern replaces conditional algorithm selection with polymorphism. Instead of if (type == "A") doA(); else if (type == "B") doB();, you encapsulate each algorithm behind a common interface and inject the desired one. This is more extensible (new algorithms don't modify existing code), more testable (strategies can be tested independently), and follows the Open/Closed Principle.
Q2: How does the Strategy pattern promote the Open/Closed Principle?
New algorithms can be added by creating new strategy classes that implement the common interface โ without modifying the context class or existing strategies. The context delegates work to the strategy interface, so it's closed for modification but open for extension.
Q3: Can you implement the Strategy pattern with lambdas in Java?
Yes. If the strategy interface has a single abstract method (functional interface), you can use lambdas or method references instead of creating full classes. For example, Comparator is a strategy interface that's commonly used with lambdas: list.sort((a, b) -> a.getName().compareTo(b.getName())). This reduces boilerplate for simple strategies.
Q4: How does the Strategy pattern differ from the State pattern?
Both encapsulate behavior behind an interface. Strategy lets the client choose which algorithm to use โ the choice is external. State manages automatic transitions between behaviors based on internal state โ the object itself changes its behavior. In Strategy, the client sets the strategy; in State, the context transitions between states on its own.
Q5: When should you prefer Strategy over Template Method?
Use Strategy when you need runtime flexibility, when the algorithms are fundamentally different (not just steps within a shared template), or when you want loose coupling through composition. Use Template Method when there's a clear algorithmic skeleton with only certain steps varying. Strategy favors composition; Template Method favors inheritance.
Advanced Editorial Pass: Strategy for Runtime Policy Evolutionโ
Strategic Payoffโ
- Encapsulates policy variation behind stable contracts.
- Enables safe runtime switching based on context, telemetry, or tenant profile.
- Reduces condition-heavy logic that is hard to test and reason about.
Non-Obvious Risksโ
- Strategy count can grow faster than governance and naming discipline.
- Context leaks too much state into strategy APIs, creating covert coupling.
- Runtime selection logic becomes a hidden second decision system.
Implementation Heuristicsโ
- Keep strategy interfaces minimal and context-agnostic.
- Externalize strategy selection policy and make it observable.
- Benchmark critical strategies; abstraction should not hide major performance cliffs.
๐ Relations with Other Patternsโ
- Bridge, State, Strategy, and Adapter: These patterns all have very similar structures based on composition: they change the behavior of the context by delegating some work to helper objects. However, they all solve different problems.
- Command: Command and Strategy may look similar because you can use both to parameterize an object with some action. However, Strategy usually defines different ways of doing the same thing, while Command converts any operation into an object.
- Template Method: Template Method is based on inheritance: it lets you alter parts of an algorithm by extending those parts in subclasses. Strategy is based on composition: you can alter parts of the object's behavior by supplying it with different strategies that correspond to that behavior.