Design Patterns Tutorial 0/70 lessons ~6 min read Lesson 32

    Strategy Pattern

    Strategy turns a conditional that selects an algorithm into a polymorphic call.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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:

    ts
    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:

    Strategy delegation
    Context
    CheckoutService
    strategy.quote()
    No switch
    ConcreteStrategy
    Injected impl
    Return result
    Polymorphic
    Context never branches on algorithm type — DI or registry picks strategy.
    Switch vs Strategy
    Variants growing?
    Git evidence
    2 stable forever?
    Function map OK
    Extract Strategy IF
    One impl per variant
    Registry at root
    Open/Closed
    Strategy earns cost when variant set is open — not for two fixed options.
    Runtime strategy selection
    Request context
    Tenant · region
    Registry lookup
    key → strategy
    Inject into Context
    Per request
    A/B via config
    Weight strategies
    Factory/registry selects strategy — Context stays unchanged.

    Informative example

    Before / after — switch sprawl to Strategy:

    ts
    // ❌ 900-line switch — every market edit risks all markets
    function quote(ctx: RideContext): Money {
    switch (ctx.city) {
    case "NYC": return nycFormula(ctx)
    case "LDN": return ldnFormula(ctx)
    // ... 40 more cases
    }
    }
    // ✅ Strategy — new market = new class
    interface 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

    1Strategy lifecycle
    1 / 4

    Extract interface

    Name the varying algorithm operation.

    quote(), score(), route().

    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.

    4 questions
    1IntermediateQuestionStrategy vs State?+

    Answer

    Strategy: context delegates to injected algorithm — externally swappable. State: context behaviour changes via internal state object representing lifecycle phase.

    Follow-up

    Order status example?
    2AdvancedQuestionStrategy vs Template Method?+

    Answer

    Strategy swaps entire algorithm. Template Method fixes algorithm skeleton in base class; subclasses override steps.

    Follow-up

    When pick Template Method?
    3IntermediateQuestionTwo variants — Strategy overkill?+

    Answer

    Often yes — function or simple if honest when set closed forever. Strategy when git shows growth.

    Follow-up

    Evidence threshold?
    4BeginnerQuestionImplement Strategy in JS without classes?+

    Answer

    Function type or object with method — same intent. Name Strategy in ADR.

    Follow-up

    Registry form?

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.