Skip to main content

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

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 = paymentStrategy;
}

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

AspectStrategyTemplate Method
MechanismComposition (has-a)Inheritance (is-a)
Algorithm structureEntire algorithm is replaceableOverall structure is fixed; steps vary
FlexibilitySwappable at runtimeFixed at compile time
CouplingLoose — strategy is a separate objectTight — subclass is bound to superclass
Use caseInterchangeable behaviorsVariations on a common workflow

Real-World Examples

Framework/LibraryDescription
java.util.ComparatorSort strategy — Collections.sort(list, comparator)
java.util.concurrent.RejectedExecutionHandlerStrategy for handling tasks rejected by a ThreadPoolExecutor
Spring ResourceClassPathResource, FileSystemResource, UrlResource — different loading strategies
javax.validationDifferent validation strategies per annotation
java.util.zipDeflater/Inflater compression strategies

Advantages & Disadvantages

AdvantagesDisadvantages
Eliminates conditional algorithm selectionClients must be aware of different strategies
Easy to extend with new algorithms (OCP)Increased number of objects
Strategies are reusable across contextsOverkill for only two variants
Runtime algorithm switchingCommunication 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.