Strategy Pattern
Strategy turns a conditional that selects an algorithm into a polymorphic call.
Introduction
Strategy turns a conditional that selects an algorithm into a polymorphic call. A client holds a Strategy interface and delegates the variable work — pricing, routing, compression, payment — to whichever implementation was injected. Adding a variant is a new class plus a registry line; no caller changes. It is the most common pattern in production because almost every system has a "how do we do X" axis that grows over time.
The story
A ride-hailing pricing module was a 900-line switch on city, day, demand, vehicle, surge, promotion. Adding a market took a week and regressed unrelated cities. Extracting PricingStrategy per city plus composable surge/promo strategies made a new market one class and one registry entry.
The business problem
Switch chains on growing algorithm axes block velocity and cause regressions:
- God-method switches: one method owns every pricing/routing/fraud variant.
- Merge conflict zones: every new market edits the same file.
- Untestable arms: testing one variant requires constructing entire context.
- No runtime swap: A/B tests and per-tenant behaviour need config-driven selection.
The problem teams faced
Strategy fits when:
- A method contains a large switch/if-else selecting an algorithm by type or context.
- Adding one variant risks regressing others in the same method.
- Variants need different collaborators (DB, API, config).
- You want A/B, feature flags, or per-tenant algorithm selection.
Understanding the topic
Intent: Encapsulate a family of algorithms and make them interchangeable.
- Strategy — interface declaring the algorithm operation.
- ConcreteStrategy — implementations (StripePricing, FestivalSurgePricing).
- Context — holds Strategy reference; delegates without branching.
Internal architecture
Strategy + Context:
interface PricingStrategy { quote(ctx: RideContext): Money }class CheckoutService {constructor(private pricing: PricingStrategy) {}total(ctx: RideContext) { return this.pricing.quote(ctx) }}class FestivalSurgePricing implements PricingStrategy {quote(ctx: RideContext) {return Money.of(ctx.km * (ctx.demand > 0.8 ? 2.4 : 1.2))}}
Visual explanation
Three diagrams cover Strategy flow, vs switch, and selection:
Informative example
Before / after — switch sprawl to Strategy:
// ❌ 900-line switch — every market edit risks all marketsfunction quote(ctx: RideContext): Money {switch (ctx.city) {case "NYC": return nycFormula(ctx)case "LDN": return ldnFormula(ctx)// ... 40 more cases}}// ✅ Strategy — new market = new classinterface PricingStrategy { quote(ctx: RideContext): Money }class CheckoutService {constructor(private pricing: PricingStrategy) {}total(ctx: RideContext) { return this.pricing.quote(ctx) }}const registry = new Map<string, PricingStrategy>()registry.set("NYC", new NycPricing())registry.set("LDN", new LdnPricing())
Execution workflow
Extract interface
Name the varying algorithm operation.
Real-world use
java.util.Comparator, array.sort comparators, Spring PasswordEncoder implementations, AWS retry strategies, Stripe payment-method handlers, A/B testing treatment strategies, every fn passed to filter/map.
Production case study
Fraud-scoring engine refactor:
- Scenario: 18 if-arms for regional fraud models; 2-week PR reviews for new models.
- Decision: FraudStrategy interface + per-model class + registry keyed by (region, version).
- Outcome: New model shipped in days; A/B became register + config weight.
Trade-offs
- Pro: Open/Closed for new variants; each strategy independently testable.
- Pro: runtime swap for A/B and feature flags.
- Con: indirection — readers hop from context to strategy.
- Con: class proliferation for 2-variant cases — function map may suffice.
Decision framework
- Use when algorithm set is open and grows in git history.
- Use when variants need different collaborators or lifecycle.
- Avoid when two stable variants forever — ternary or function honest.
- Avoid when variants share most logic — Template Method may fit.
Best practices
- Name Strategy in ADR even when implementation is a function type in TS.
- Registry at composition root — not switch inside Context.
- Contract tests shared across all strategy implementations.
Anti-patterns to avoid
- Strategy with one implementation forever — YAGNI until second variant exists.
- Context that still switches on type despite Strategy injection.
- God-strategy doing unrelated algorithms — split interfaces (ISP).
Common mistakes
- Extracting Strategy before second variant exists — premature abstraction.
- Registry key collisions between modules.
- Strategy holding mutable shared state across requests.
Debugging tips
- Log strategy class name at selection — trace wrong-pricing bugs.
- Diff registry contents between prod and staging.
Optimization strategies
- Cache strategy selection when stable per request/tenant.
- Function strategy for single-method algorithms in TS — less ceremony.
Common misconceptions
- Strategy ≠ any interface. Need evidence of varying algorithm axis.
- State vs Strategy: State changes behaviour by internal phase; Strategy by injected algorithm.
- Every Comparator is Strategy — classic example.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1IntermediateQuestionStrategy vs State?+
Answer
Follow-up
2AdvancedQuestionStrategy vs Template Method?+
Answer
Follow-up
3IntermediateQuestionTwo variants — Strategy overkill?+
Answer
Follow-up
4BeginnerQuestionImplement Strategy in JS without classes?+
Answer
Follow-up
Summary
You can collapse switch chains into Strategy, wire registries at the composition root, and distinguish Strategy from State. Tell the 900-line pricing switch story — if you can do that, you own Strategy.