← System Design
🎯
System Design

Strategy

Swap the algorithm at runtime. Pick FIFO, LIFO, or priority queueing without rewriting the caller.

TL;DR

Strategy turns an algorithm into a pluggable object. Instead of `if (kind === 'fifo') ...; else if (kind === 'lifo') ...`, you define a `QueueingStrategy` interface and inject a concrete one. The caller stays the same; the behavior changes. This is how you avoid `switch` statements that grow forever, and the canonical way to satisfy Open/Closed.

When to use

Whenever the same problem can be solved by multiple interchangeable algorithms and the choice depends on input, configuration, or A/B test bucket. Common in compression (gzip vs zstd), pricing (regular vs sale vs bulk), authentication (password vs OAuth vs SSO), routing (round-robin vs least-conn vs hash-based), and retry policies.

Try it

The pattern in code

interface SortStrategy<T> { sort(items: T[]): T[]; }

class QuickSort<T> implements SortStrategy<T> { sort(items: T[]) { /* ... */ } }
class MergeSort<T> implements SortStrategy<T> { sort(items: T[]) { /* ... */ } }
class TimSort<T>   implements SortStrategy<T> { sort(items: T[]) { /* ... */ } }

class Pipeline<T> {
  constructor(private strategy: SortStrategy<T>) {}
  process(items: T[]) { return this.strategy.sort(items); }
}

// Pick at runtime — same Pipeline, different behavior.
const fast   = new Pipeline(new QuickSort());
const stable = new Pipeline(new MergeSort());

Why it matters in system design

The classic “if/else jungle” is what Strategy was invented to kill. Picture a load balancer:

if (mode === 'rr') ... 30 lines ...
else if (mode === 'least_conn') ... 40 lines ...
else if (mode === 'p2c') ... 25 lines ...
else if (mode === 'hash') ... 35 lines ...

A new mode forces a code change in this one giant function. Tests have to cover every branch. The cyclomatic complexity is unbounded.

Strategy vs State — same shape, opposite intent

Strategy in distributed systems

Strategy shows up everywhere supply meets demand:

  • Load balancing — round-robin, least-connections, weighted, P2C
  • Partitioning — hash, range, consistent hashing
  • Retry policies — no-retry, fixed, exponential backoff with jitter
  • Serialization formats — JSON, Protobuf, Avro
  • Cache eviction — LRU, LFU, TTL

All of these are Strategy implementations chosen via config.

Strategy + Factory = real systems

Trade-offs

Pros: Open/Closed compliant; each algorithm is independently testable; easy A/B test rollout; zero switch growth.

📺 Video

A YouTube walkthrough — coming once the deep-dive video is published.

🧪 Simulator

An interactive simulator for this concept is on the way — tweak the knobs, watch behaviour change in real time.

💻 Code

A 30-line build challenge with starter code, hints, and a reference implementation.

🎯 Common interview questions
Q1. How is Strategy different from polymorphism in general?

Strategy is *targeted* polymorphism — the abstraction exists *just* to make an algorithm swappable. The class hierarchy isn't modeling a domain entity; it's modeling "ways to do this one thing." That focus matters because it tells you when to introduce the abstraction (when you have or expect multiple algorithms) and when not to (a single concrete type → just call it directly).

Q2. Strategy vs State pattern?

Mechanically very similar — both inject a behavior object. Conceptually opposite — Strategy is chosen by the *caller* and tends to be stable for the lifetime of an operation; State is chosen by the *object itself* and changes as the object's lifecycle progresses (Closed → Open → Half-Open in a circuit breaker).

Q3. Where does Strategy show up in distributed systems?

Load balancing algorithms (round-robin, least-connections, weighted, P2C), partitioning strategies (hash, range, consistent hashing), retry policies (no retry, fixed, exponential backoff with jitter), serialization formats (JSON, Protobuf, Avro), eviction policies in caches (LRU, LFU, TTL). All of these are Strategy implementations chosen via config.

Q4. How do you pick which strategy to inject?

Three common patterns — (1) constructor injection chosen at startup based on config, (2) a Factory that maps a type/string to the right strategy, (3) the strategy itself being a function (in languages with first-class functions, "Strategy" often degenerates to "pass a callback"). All three are valid; the right one depends on whether the strategy needs setup state.

Q5. When does Strategy become over-engineering?

When there's only ever going to be one algorithm. Don't write `IPricingStrategy` with a single `RegularPricing` implementation just in case. Wait until the second algorithm appears and *then* extract — refactoring two concrete cases into an interface is mechanical; predicting which interface you need before the second case exists is guessing.

↗ Related concepts

Comments 0

Discuss this page. Markdown supported. Be kind.

Loading…
Loading comments…