Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it — swap sorting, routing, pricing, or compression at runtime.

The Problem

You’re building a navigator app. It needs three routing modes: driving (fastest route), walking (shortest distance), cycling (bike paths). The naive approach puts them all in one massive Navigator class:

class Navigator {
  buildRoute(from: string, to: string, mode: string): Route {
    if (mode === 'driving') { /* ... */ }
    else if (mode === 'walking') { /* ... */ }
    else if (mode === 'cycling') { /* ... */ }
    // add transit, ferry, etc. → keeps growing
  }
}

Every new mode means editing Navigator. Any bug in cycling code risks breaking driving code. The class can’t be unit tested in isolation per algorithm.


The Solution

Extract each algorithm into its own class behind a common interface. The context (Navigator) holds a reference to a strategy and delegates to it. The client picks the strategy at runtime.

Context - strategy: Strategy + doWork() «interface» Strategy + execute(data): Result StrategyA + execute() StrategyB + execute() swap strategy at runtime
Strategy — Context holds a Strategy reference that can be swapped at runtime; concrete strategies are interchangeable

Real-World Analogy

Sorting a deck of cards. You can sort by suit, by rank, or by value. The “sort” operation is the same; the comparison strategy differs. A card game app might swap sorting strategies depending on which game mode is active — without rewriting the “sort” code.


TypeScript Example

// Strategy interface
interface SortStrategy<T> {
  sort(data: T[]): T[];
  name: string;
}

// Concrete strategies
class BubbleSortStrategy<T> implements SortStrategy<T> {
  readonly name = 'Bubble Sort';

  sort(data: T[]): T[] {
    const arr = [...data];
    for (let i = 0; i < arr.length - 1; i++) {
      for (let j = 0; j < arr.length - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
          [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        }
      }
    }
    return arr;
  }
}

class QuickSortStrategy<T> implements SortStrategy<T> {
  readonly name = 'Quick Sort';

  sort(data: T[]): T[] {
    if (data.length <= 1) return data;
    const pivot = data[Math.floor(data.length / 2)];
    const left  = data.filter(x => x < pivot);
    const mid   = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    return [...this.sort(left), ...mid, ...this.sort(right)];
  }
}

class InsertionSortStrategy<T> implements SortStrategy<T> {
  readonly name = 'Insertion Sort';

  sort(data: T[]): T[] {
    const arr = [...data];
    for (let i = 1; i < arr.length; i++) {
      const key = arr[i];
      let j = i - 1;
      while (j >= 0 && arr[j] > key) {
        arr[j + 1] = arr[j];
        j--;
      }
      arr[j + 1] = key;
    }
    return arr;
  }
}

// Context
class DataSorter<T> {
  constructor(private strategy: SortStrategy<T>) {}

  setStrategy(strategy: SortStrategy<T>): void {
    console.log(`[Sorter] Switching to ${strategy.name}`);
    this.strategy = strategy;
  }

  sort(data: T[]): T[] {
    console.log(`[Sorter] Sorting ${data.length} items with ${this.strategy.name}`);
    const start  = performance.now();
    const result = this.strategy.sort(data);
    const ms     = (performance.now() - start).toFixed(3);
    console.log(`[Sorter] Done in ${ms}ms`);
    return result;
  }
}

// Real-world usage: pick strategy based on data characteristics
function pickSortStrategy<T>(data: T[]): SortStrategy<T> {
  if (data.length < 20)  return new InsertionSortStrategy<T>(); // fast for tiny arrays
  if (data.length < 1000) return new QuickSortStrategy<T>();     // general purpose
  return new QuickSortStrategy<T>();                              // merge sort in practice
}

const numbers = [64, 34, 25, 12, 22, 11, 90, 3, 7, 45, 18];
const sorter  = new DataSorter(new BubbleSortStrategy<number>());

console.log('With bubble sort:', sorter.sort(numbers));

sorter.setStrategy(new QuickSortStrategy<number>());
console.log('With quick sort:', sorter.sort(numbers));

// Swap automatically based on input size
const bigArray = Array.from({ length: 500 }, () => Math.floor(Math.random() * 1000));
sorter.setStrategy(pickSortStrategy(bigArray));
sorter.sort(bigArray);

When to Use

  • You want to swap algorithms or behaviours at runtime
  • You have a class with a large conditional that selects between variations of the same algorithm
  • You want to isolate algorithm implementation details from the code that uses them
  • You need to test algorithms independently
  • Examples: sorting strategies, payment processors, compression algorithms, routing modes, discount policies, authentication methods

Pros

  • Open/Closed — add new strategies without touching the context
  • Swap algorithms at runtime
  • Isolate algorithm details from client code — each strategy is independently testable
  • Replace inheritance with composition — no need for subclasses to vary behaviour

Cons

  • Client code must know about different strategies to choose between them
  • Functional languages can often pass a function instead of a whole strategy object — Strategy can feel heavy in those contexts
  • Creates more classes — overkill if you only have two strategies that rarely change

  • State is structurally identical but transitions itself; Strategy is selected by the client
  • Command encapsulates a request (with history/undo); Strategy encapsulates an algorithm (no history)
  • Template Method uses inheritance to vary algorithm steps; Strategy uses composition to swap whole algorithms
  • Decorator adds behaviour without changing the interface; Strategy changes how something is done