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.
Pattern Structure

Implementation: Pluggable Sorting
package strategy;
/** Strategy interface — all sorting algorithms implement this */
public interface SortStrategy {
void sort(int[] data);
String getName();
}
package strategy;
public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] data) {
System.out.println(" [BubbleSort] O(n²) — for small arrays");
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"; }
}
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);
}
// ... divide and conquer implementation ...
@Override public String getName() { return "MergeSort"; }
}
public class QuickSort implements SortStrategy {
@Override
public void sort(int[] data) {
System.out.println(" [QuickSort] O(n log n) avg — fastest in practice");
quickSort(data, 0, data.length - 1);
}
// ... partition-based implementation ...
@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.
package strategy;
import java.util.Arrays;
/**
* Context — uses a SortStrategy. Strategy can be changed at runtime.
* Sorter has no idea which algorithm it's using — 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); // don't mutate the input
strategy.sort(copy);
return copy;
}
}
public class Main {
public static void main(String[] args) {
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("Result: " + Arrays.toString(sorter.sort(data)));
sorter.setStrategy(new MergeSort());
System.out.println("\n-- MergeSort --");
System.out.println("Result: " + Arrays.toString(sorter.sort(data)));
sorter.setStrategy(new QuickSort());
System.out.println("\n-- QuickSort --");
System.out.println("Result: " + Arrays.toString(sorter.sort(data)));
// Runtime selection based on data size
int size = 10_000;
SortStrategy chosen = size > 1000 ? new QuickSort() : new BubbleSort();
sorter.setStrategy(chosen);
System.out.println("\n Auto-selected " + chosen.getName() + " for n=" + size);
}
}
Console Output
Input: [64, 34, 25, 12, 22, 11, 90]
— BubbleSort —
[BubbleSort] O(n²) — for small arrays
Result: [11, 12, 22, 25, 34, 64, 90]
— MergeSort —
Switching to: MergeSort
[MergeSort] O(n log n) stable — good for large datasets
Result: [11, 12, 22, 25, 34, 64, 90]
— QuickSort —
Switching to: QuickSort
[QuickSort] O(n log n) avg — fastest in practice
Result: [11, 12, 22, 25, 34, 64, 90]
— Runtime strategy selection —
Switching to: QuickSort
Auto-selected QuickSort for n=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
- Template Method Design Pattern in Java — fixes the algorithm skeleton, varies the steps; Strategy varies the whole algorithm
- State Design Pattern in Java — identical structure, different intent: State transitions itself; Strategy is swapped by the caller
- Decorator Design Pattern in Java — adds behaviour to existing interface; Strategy replaces the core algorithm entirely
Further Reading
- Strategy — Refactoring.Guru
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma et al., Chapter 5