What is the Strategy design pattern, and what problem does it solve?
answer
- family of interchangeable algorithms
- Strategy / ConcreteStrategy / Context
- delegate, don't branch
- composition over inheritance, OCP
- client (factory/DI) picks the variant
basics
~20 sStrategy defines a family of interchangeable algorithms, puts each one behind the same interface, and lets the calling code pick which one to use at runtime — so you swap behavior instead of editing a big if/else.
solid answer
~40 sStrategy is a behavioral pattern with three roles: a **Strategy** interface declaring one operation (e.g. `sort(list)`, `price(order)`), several **Concrete Strategies** implementing it differently, and a **Context** that holds a reference to a Strategy and delegates to it rather than implementing the behavior itself. The client injects the chosen strategy; the Context never knows which one it got. The problem it solves: a class that hard-codes several variants of an algorithm in conditionals becomes big, hard to test, and must be edited every time a variant is added. Strategy makes the algorithm vary independently of the clients that use it — each variant is separately testable, and adding one means adding a class, not modifying existing code (Open/Closed). Cost: more types, and the client must now know enough to choose.
code
typescript · 14 linesinterface ShippingStrategy { cost(o: Order): number }
const standard: ShippingStrategy = { cost: o => 5 + 0.5 * o.weightKg };
const express: ShippingStrategy = { cost: o => 15 + 1.2 * o.weightKg };
const pickup: ShippingStrategy = { cost: () => 0 };
class Checkout {
constructor(private shipping: ShippingStrategy) {}
total(o: Order) { return o.subtotal + this.shipping.cost(o); } // no if/else
}
// client chooses at runtime
const registry = { STANDARD: standard, EXPRESS: express, PICKUP: pickup };
new Checkout(registry[request.shippingMethod]).total(order);go deeper
Name the intent (interchangeable algorithms behind one interface), the three roles, and give a concrete example such as a comparator passed to sort.
Add the mechanism: composition over inheritance, delegation, runtime swapping, and where the selection lives (factory/registry/DI). Mention it enables Open/Closed.
Discuss interface design for the whole family, testing benefits, decorating strategies for cross-cutting concerns, and honestly state when a conditional is the better design.
Frame it as a pluggable-policy seam: extension points for other teams/plugins, config- or tenant-driven selection, versioning of policies, observability of which policy ran, and the cost of freezing a bad interface too early.
## The problem Imagine one class that must do the *same job* in several different *ways*. A checkout service computes shipping cost, but the computation differs for standard, express, and pickup. The naive version puts all three inside one method: ``` function shippingCost(order, method): if method == STANDARD: ...20 lines... else if method == EXPRESS: ...25 lines... else if method == PICKUP: return 0 ``` Symptoms of this design: - The method grows every time a variant is added, so the class has many reasons to change. - All variants share one scope, so their local details leak into each other. - You cannot test one variant without going through the whole method and constructing an order that reaches the right branch. - Callers who want a *new* variant must edit code they don't own. ## The pattern **Strategy** (from the "Gang of Four" catalogue, in the *behavioral* group — patterns about how objects distribute responsibility and communicate) says: *define a family of algorithms, encapsulate each one, and make them interchangeable; Strategy lets the algorithm vary independently from clients that use it.* Three roles: 1. **Strategy** — an interface (or abstract type, or just a function type) declaring the operation, e.g. `ShippingStrategy { cost(order): Money }`. This is the *contract* every variant honors: same inputs, same kind of output, same meaning. 2. **Concrete Strategy** — one implementation per variant: `StandardShipping`, `ExpressShipping`, `PickupShipping`. Each is a small, independently testable unit. 3. **Context** — the object that *uses* a strategy. It holds a reference to a `Strategy` and delegates: `class Checkout(strategy) { total(order) = order.subtotal + strategy.cost(order) }`. Crucially the Context is written against the interface and never branches on which concrete type it holds. The **client** (whoever wires things up — a factory, a dependency-injection container, a controller reading a request field, a config file) chooses the concrete strategy and hands it in, via constructor or setter. ### Terms used above - *Interface / contract*: a declaration of operations without implementation; several classes can implement it, and code depending on the interface works with any of them. This is **polymorphism**: one call site, many possible behaviors, resolved at runtime. - *Delegation*: object A does its job by calling object B instead of doing the work itself. - *Open/Closed Principle (OCP)*: modules should be open to extension (new behavior can be added) but closed to modification (without editing existing, already-tested code). Strategy is the textbook mechanism for OCP — a new algorithm is a new class. - *Composition over inheritance*: Strategy varies behavior by *holding* an object, not by subclassing the Context. That means behavior can change at runtime and one Context can combine several independent strategy axes without a combinatorial subclass explosion. ## Where you have already used it Strategy is everywhere in standard libraries, usually without the name: - A **comparator/key function** passed to a sort routine — the sorting algorithm is fixed, the *ordering* is the strategy. - A **retry/backoff policy** object handed to an HTTP client. - A **serialization format** (JSON/XML/protobuf writer) chosen per request. - A **compression codec** selected by file extension. - A **password hashing algorithm** selected by the prefix stored with a hash. ## Trade-offs **Wins**: each algorithm is isolated and unit-testable in isolation; the Context shrinks and stops changing; variants can be added by other teams/plugins; a variant can be swapped at runtime (per request, per tenant, per feature flag); you can wrap or decorate a strategy (add caching, logging, metrics) without touching the Context. **Costs**: more types and more indirection — for two three-line branches that never change, a conditional is honestly better; the *client* now needs knowledge to pick a strategy (often pushed into a factory or a registry map keyed by an enum/string); the interface must be designed for the *whole family*, which is hard if variants need different inputs (see the parameter-bloat problem); an extra object allocation and a virtual call, which almost never matters but occasionally does in hot loops. **When not to use it**: the variants will never grow and are trivial; or the "variants" actually differ in more than one operation and belong to a richer abstraction; or the selection itself is the only complexity (then a lookup table of values, not of algorithms, is enough). ## Minimal shape ``` interface PricingStrategy { price(order): Money } class FlatRate(rate) implements PricingStrategy { price(o) = rate } class WeightBased(perKg) implements PricingStrategy { price(o) = o.weightKg * perKg } class Checkout(pricing: PricingStrategy) { total(o) = o.subtotal + pricing.price(o) // no branching, ever } ``` If the Context still contains `if (strategy is WeightBased)` anywhere, the pattern has been broken — that conditional is exactly what Strategy was meant to remove.
- Where does the decision of *which* strategy to use live, if not in the Context?In the client: a factory, a registry/map keyed by an enum or config value, or a dependency-injection container. The branching doesn't vanish — it collapses into one small selection point instead of being duplicated at every use site.
- Does Strategy require an interface with exactly one method?No, but one cohesive operation is the common and healthiest case. Multi-method strategies drift toward Bridge or a full abstraction; if two methods are always overridden together they may be one method taking a richer argument.
- How does Strategy differ from plain subclassing of the Context?Subclassing binds the variant at compile time and consumes the single inheritance slot; Strategy composes, so behavior can be swapped at runtime, reused by several contexts, and combined along multiple independent axes.
Getting from the airport to a hotel: taxi, train, or walking are interchangeable strategies behind one interface — getMeThere(destination). You (the context) don't rewrite your travel plan for each; you pick one at the door based on cost, weather, luggage, and the rest of the trip is unchanged.
saying these in an interview costs you the question
- Saying the Context should `instanceof`-check or switch on the concrete strategy — that reintroduces the conditional the pattern removes.
- Claiming Strategy is about *reusing* code between variants; that is Template Method's job. Strategy is about *replacing* whole algorithms.
- Thinking each strategy must be a class — a function/lambda is a perfectly valid strategy in languages with first-class functions.
- Assuming Strategy removes all conditionals; it centralizes the choice into one factory/registry.
- Applying it to two trivial, stable branches, adding three files for four lines of logic.