skip to content

questions

6

What is the Strategy design pattern, and what problem does it solve?

level: juniorimportance: must knowfreq 72%

answer

  1. family of interchangeable algorithms
  2. Strategy / ConcreteStrategy / Context
  3. delegate, don't branch
  4. composition over inheritance, OCP
  5. client (factory/DI) picks the variant

basics

~20 s

Strategy 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 s

Strategy 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 lines
typescript
interface 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

for a junior

Name the intent (interchangeable algorithms behind one interface), the three roles, and give a concrete example such as a comparator passed to sort.

for a middle

Add the mechanism: composition over inheritance, delegation, runtime swapping, and where the selection lives (factory/registry/DI). Mention it enables Open/Closed.

for a senior

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.

for a principal

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.

context

open as a page

The Strategy and State patterns have nearly identical class diagrams — a context delegating to an interface with several implementations. How do they actually differ, and what does that difference change in the code?

level: middleimportance: must knowfreq 58%

basics

~20 s

Same structure, different intent. In Strategy the client picks one algorithm and it usually stays fixed; the variants don't know about each other. In State the object's own behavior changes as its lifecycle advances, and states typically trigger the transitions to the next state.

open as a page

How would you refactor a long conditional that dispatches among several algorithms into the Strategy pattern, and where does the runtime selection of the strategy end up living?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Extract each branch into its own object implementing one shared interface, replace the branching call site with a delegation, and move the choice into one place — a factory or a map from key to strategy — that the client uses to pick the right implementation.

open as a page

In a language with first-class functions, when is a plain function or lambda a sufficient implementation of the Strategy pattern, and when do you still want a named interface with classes?

level: middleimportance: should knowfreq 44%

basics

~20 s

If the strategy is one operation with no state and no extra methods, a function value is the whole pattern — a lambda is an object with one method. Prefer a named interface or class when the strategy needs configuration, identity, a name for DI/registry lookup, or more than one operation.

open as a page

Compare Strategy with Template Method and with Bridge: what does each vary, by what mechanism, and how do you choose between them?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Strategy swaps a whole algorithm via composition, chosen at runtime. Template Method fixes the algorithm's skeleton in a base class and lets subclasses override individual steps, bound at compile time by inheritance. Bridge splits an abstraction hierarchy from an implementation hierarchy so both can grow independently.

open as a page

You are designing a pluggable-policy extension point (for example a pricing, ranking, or fraud-scoring policy) that other teams will implement using the Strategy pattern. What design and operational concerns go beyond the textbook pattern?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Treat the strategy interface as a published API: keep its inputs a single evolvable object, make implementations stateless and side-effect-free, define how one is selected and what happens on unknown or failing policies, and make it observable — always log which policy ran.

open as a page