Skip to main content

Strategy Design Pattern in Java: Complete Guide with Examples

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. No more switch statements for algorithm selection. Complete Java guide: sorting strategy with BubbleSort/MergeSort/QuickSort, runtime selection, Java 8 functional shortcut with Comparator, and when Strategy is overkill.

A navigation app calculates routes. For walking, shortest distance matters most. For driving, traffic matters. For cycling, elevation change and bike lanes matter. If you put all three algorithms in one class and switch between them with a flag, every algorithm change requires recompiling and re-testing the whole class — and adding a fourth mode means touching the existing code. The Strategy pattern encapsulates each algorithm in its own class and lets the caller swap them at runtime, with zero changes to the surrounding code.

This is one of the most practical patterns in the GoF catalogue. Java’s Comparator, Comparator.comparing(), and the entire java.util.function package are functional implementations of the Strategy pattern. Understanding the structural form first makes the lambda shorthand more meaningful.

All code compiles and runs with Java 25. No external dependencies required.

Pattern Structure

Strategy design pattern structure (via refactoring.guru)
Context holds a Strategy reference. Concrete Strategies implement the algorithm. The client selects and injects the strategy. Diagram: refactoring.guru

Implementation: Pluggable Sorting

Every sorting algorithm implements the same two-method contract, so the context can call any of them identically:

SortStrategy.java
package strategy;

/** Strategy interface — all sorting algorithms implement this */
public interface SortStrategy {
    void sort(int[] data);
    String getName();
}

BubbleSort is the simplest and slowest — O(n²), fine for small arrays:

BubbleSort.java
package strategy;

public class BubbleSort implements SortStrategy {
    @Override
    public void sort(int[] data) {
        System.out.println("  [BubbleSort] O(n²) — small arrays only");
        for (int i = 0; i < data.length - 1; i++)
            for (int j = 0; j < data.length - 1 - i; j++)
                if (data[j] > data[j + 1]) { int t = data[j]; data[j] = data[j+1]; data[j+1] = t; }
    }
    @Override public String getName() { return "BubbleSort"; }
}

MergeSort is O(n log n) and stable — good for large datasets where ordering of equal elements matters:

MergeSort.java
package strategy;

import java.util.Arrays;

public class MergeSort implements SortStrategy {
    @Override
    public void sort(int[] data) {
        System.out.println("  [MergeSort] O(n log n) — stable, good for large datasets");
        mergeSort(data, 0, data.length - 1);
    }

    private void mergeSort(int[] a, int l, int r) {
        if (l >= r) return;
        int m = (l + r) / 2;
        mergeSort(a, l, m); mergeSort(a, m + 1, r);
        merge(a, l, m, r);
    }

    private void merge(int[] a, int l, int m, int r) {
        int[] left = Arrays.copyOfRange(a, l, m + 1);
        int[] right = Arrays.copyOfRange(a, m + 1, r + 1);
        int i = 0, j = 0, k = l;
        while (i < left.length && j < right.length)
            a[k++] = left[i] <= right[j] ? left[i++] : right[j++];
        while (i < left.length)  a[k++] = left[i++];
        while (j < right.length) a[k++] = right[j++];
    }

    @Override public String getName() { return "MergeSort"; }
}

QuickSort is O(n log n) on average and the fastest in practice, but not stable:

QuickSort.java
package strategy;

public class QuickSort implements SortStrategy {
    @Override
    public void sort(int[] data) {
        System.out.println("  [QuickSort] O(n log n) avg — fast in practice, not stable");
        quickSort(data, 0, data.length - 1);
    }

    private void quickSort(int[] a, int lo, int hi) {
        if (lo >= hi) return;
        int p = partition(a, lo, hi);
        quickSort(a, lo, p - 1);
        quickSort(a, p + 1, hi);
    }

    private int partition(int[] a, int lo, int hi) {
        int pivot = a[hi], i = lo;
        for (int j = lo; j < hi; j++)
            if (a[j] <= pivot) { int t = a[i]; a[i++] = a[j]; a[j] = t; }
        int t = a[i]; a[i] = a[hi]; a[hi] = t;
        return i;
    }

    @Override public String getName() { return "QuickSort"; }
}

The Sorter context holds a reference to whichever strategy is currently active. It copies the input array before sorting so the original remains unchanged — making it easy to demonstrate that different strategies produce the same result.

Sorter.java
package strategy;

import java.util.Arrays;

/**
 * Context — uses a SortStrategy. The strategy can be swapped at runtime.
 * Sorter doesn't care which algorithm is used; it just calls sort().
 */
public class Sorter {
    private SortStrategy strategy;

    public Sorter(SortStrategy strategy) { this.strategy = strategy; }

    public void setStrategy(SortStrategy strategy) {
        System.out.println("  Switching to: " + strategy.getName());
        this.strategy = strategy;
    }

    public int[] sort(int[] data) {
        int[] copy = Arrays.copyOf(data, data.length);
        strategy.sort(copy);
        return copy;
    }
}

Main sorts the same array three times with three different strategies, then simulates choosing a strategy based on input size:

Main.java
package strategy;

import java.util.Arrays;

/**
 * Strategy Design Pattern — Runnable Demo
 * Run: javac strategy/*.java -d out/strategy && java -cp out/strategy strategy.Main
 * Article: https://ankurm.com/strategy-design-pattern-java/
 */
public class Main {
    public static void main(String[] args) {
        System.out.println("=== Strategy Design Pattern Demo ===\n");

        int[] data = {64, 34, 25, 12, 22, 11, 90};
        System.out.println("Input: " + Arrays.toString(data));

        Sorter sorter = new Sorter(new BubbleSort());
        System.out.println("\n-- BubbleSort --");
        System.out.println("Sorted: " + Arrays.toString(sorter.sort(data)));

        sorter.setStrategy(new MergeSort());
        System.out.println("\n-- MergeSort --");
        System.out.println("Sorted: " + Arrays.toString(sorter.sort(data)));

        sorter.setStrategy(new QuickSort());
        System.out.println("\n-- QuickSort --");
        System.out.println("Sorted: " + Arrays.toString(sorter.sort(data)));

        System.out.println("\n-- Runtime strategy selection (simulating large dataset) --");
        int size = 10_000;
        SortStrategy chosen = size > 1000 ? new QuickSort() : new BubbleSort();
        sorter.setStrategy(chosen);
        System.out.println("  Chose " + chosen.getName() + " for size=" + size);

        System.out.println("\n=== Demo complete ===");
    }
}

Console Output

=== Strategy Design Pattern Demo ===

Input: [64, 34, 25, 12, 22, 11, 90]

— BubbleSort —
  [BubbleSort] O(n²) — small arrays only
Sorted: [11, 12, 22, 25, 34, 64, 90]
  Switching to: MergeSort

— MergeSort —
  [MergeSort] O(n log n) — stable, good for large datasets
Sorted: [11, 12, 22, 25, 34, 64, 90]
  Switching to: QuickSort

— QuickSort —
  [QuickSort] O(n log n) avg — fast in practice, not stable
Sorted: [11, 12, 22, 25, 34, 64, 90]

— Runtime strategy selection (simulating large dataset) —
  Switching to: QuickSort
  Chose QuickSort for size=10000

=== Demo complete ===

Strategy in Java 8+: Lambdas as Strategies

In Java 8+, a functional interface is a Strategy interface, and a lambda is a Strategy implementation. The full class hierarchy is optional for simple cases.

// SortStrategy is a functional interface (one abstract method)
// so we can use lambdas as strategies directly
Sorter sorter = new Sorter(data -> Arrays.sort(data));  // JDK's sort as a strategy
// java.util.Comparator is the canonical Strategy example in the JDK
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// Strategy 1: alphabetical
names.sort(Comparator.naturalOrder());
// Strategy 2: by length, then alphabetical
names.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
// Strategy 3: custom business rule — treat empty strings as highest
names.sort((a, b) -> {
    if (a.isEmpty()) return 1;
    if (b.isEmpty()) return -1;
    return a.compareTo(b);
});
// Each Comparator is a concrete Strategy. Collections.sort() is the Context.

💡 Strategy vs Template Method: Both patterns define algorithms that can vary. Strategy varies the whole algorithm — you swap the entire implementation. Template Method varies parts of an algorithm — it defines the skeleton in an abstract class and delegates specific steps to subclasses. Use Strategy when the algorithm is completely interchangeable (any sorting algorithm produces the same result); use Template Method when the algorithm has a fixed skeleton with customisable steps (every data migration connects, reads, transforms, writes — but each source connects differently).

Strategy in the JDK

Comparator<T> — sorting strategy for Collections.sort() and List.sort(). ExecutorService — thread execution strategy for Executor.execute(). ThreadFactory — thread creation strategy. javax.xml.parsers.SAXParserFactory and DocumentBuilderFactory — parser selection strategy. Every @FunctionalInterface in java.util.function (Predicate, Function, Consumer) is a Strategy interface; lambdas are the concrete strategies.

When to Use Strategy

Reach for Strategy when: you have multiple variants of an algorithm and want to switch between them at runtime without conditionals. You want to isolate algorithm-specific code from the client code that uses it. The algorithms share the same interface but have completely different implementations. You’re building a library and want clients to inject custom behaviour.

Avoid it when: you only have two or three strategies that never change — a simple if-else or enum is clearer. In Java 8+, many Strategy use cases are better served by passing a lambda — no dedicated class needed. The strategies need access to private state of the Context — that tight coupling undermines the separation the pattern provides.

✅ Stateless Strategies are Shareable: If a concrete strategy holds no mutable state (sorting algorithms don’t need to remember anything between calls), a single instance can be safely shared across threads and contexts. Create them once, store them as constants or inject them via Spring, and reuse without synchronisation concerns. Stateful strategies (ones that accumulate results or maintain a cursor) must be created per-use or explicitly synchronized.

Runnable Code on GitHub

Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/strategy/.

javac 03-behavioral/strategy/*.java -d out/strategy
java -cp out/strategy strategy.Main

See Also

Further Reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.