Behavioral

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable — let the algorithm vary independently from clients that use it.

Beginner Pattern #21 of 23

What is the Strategy Pattern?

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.

Real-world analogy: Navigation app. You can choose to navigate by car, by bike, or on foot. The routing algorithm (strategy) changes, but the app (context) stays the same — it just calls buildRoute(A, B) on whichever strategy is selected.

The Problem It Solves

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.

Participants

Strategy (interface)
Declares the algorithm interface common to all supported strategies. Context uses this interface to call the algorithm defined by each ConcreteStrategy.
ConcreteStrategy
Implements the algorithm using the Strategy interface. Each ConcreteStrategy encapsulates one specific algorithm.
Context
Maintains a reference to a Strategy. Delegates the algorithm to the Strategy object. May provide an interface for clients to swap the strategy at runtime.

Visual Flow Diagram

Context strategy: Strategy setStrategy(s) execute() → strategy.do() «interface» Strategy execute(data) BubbleSort execute(data) QuickSort execute(data) MergeSort execute(data) Context.setStrategy(new QuickSort()) → can swap at runtime

Java Code Example

Java — Sorting + Payment Strategies
// ── 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));

When to Use / Avoid

✓ Use When

  • Multiple related classes differ only in their behaviour — strategies factored out
  • You need different variants of an algorithm and want to switch at runtime
  • Algorithm uses data the client shouldn't know about
  • A class defines many behaviours and they appear as conditional statements

✕ Avoid When

  • Only one or two algorithms exist and they rarely change — overkill
  • Algorithms are trivially simple — just use a lambda or method reference
  • Clients must know about all strategies to select one — may not always be appropriate

Real-World Examples

Pros & Cons

Pros

  • Eliminates conditional statements — each branch becomes a class
  • Open/Closed — add new strategies without changing Context
  • Swap algorithms at runtime
  • Strategies can be tested in isolation, independently of Context

Cons

  • Clients must know about different strategies to select one
  • Increased number of objects — one class per strategy
  • In modern Java, functional interfaces + lambdas often make Strategy classes unnecessary

How Strategy Can Be Broken

⚠ Attack Vectors

  • Null strategy: Context's strategy field is never set — calling execute() throws NullPointerException with no useful error message
  • Context leaking internal data to strategy: Strategy needs to call back into the Context to get data — creates a circular dependency and tight coupling between Strategy and Context
  • Stateful shared strategy: A ConcreteStrategy stores per-execution state in its instance fields — when the same strategy object is used by multiple Contexts concurrently, state is corrupted
  • instanceof checks to pick strategy behaviour: Context uses if (strategy instanceof QuickSort) to add special-case logic — defeats the point of the pattern entirely

✓ Prevention

  • Default strategy or null guard: Provide a sensible default in the constructor, or throw IllegalStateException("No strategy configured") with a clear message in execute() before calling the strategy
  • Pass all data as parameters: Strategy's execute(data) receives everything it needs as method parameters — no callbacks into Context, no circular dependency
  • Keep strategies stateless: All per-execution data should be local variables inside execute() — no instance fields that persist between calls. Stateless strategies are safe to share
  • Never instanceof the strategy: If you need different behaviour depending on which strategy is active, that is a signal to add a new method to the Strategy interface — or reconsider the design
Java — Break & Fix
// ❌ 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
}

How Other Patterns Relate

Interview Cheat Sheet

  1. What: Encapsulates a family of algorithms into separate classes. Context holds a Strategy reference and delegates to it. Client picks and swaps strategy at runtime without changing Context.
  2. How: Strategy interface with one method. ConcreteStrategies implement it. Context holds strategy: Strategy, calls strategy.execute(data). Modern Java: use lambdas/method refs for stateless simple strategies.
  3. Key distinctions: vs State — client picks strategy, object transitions state. vs Template Method — Strategy uses composition/whole-algorithm swap; Template Method uses inheritance/step override.