Chapter 05 · Article 23 of 55

Strategy Pattern - Deep Dive

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Article outline14 sections on this page

Overview

Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Also Known As: Policy Pattern

Pattern Type: Behavioural (GoF)

The Strategy Pattern is one of the most widely used behavioural design patterns. It captures the idea that algorithms (or behaviours) should be extracted into their own classes, each implementing a common interface, so that the client (called the Context) can switch between them at runtime without knowing the implementation details. This promotes the Open/Closed Principle - you can introduce new strategies without modifying existing code.

At its core, Strategy replaces conditional branching with polymorphism. Instead of a growing if-else or switch block that selects an algorithm, you delegate to an interchangeable object that is the algorithm.


Problem It Solves

The Conditional Logic Explosion

Consider a payment processing module that supports credit cards, PayPal, and cryptocurrency. A naïve implementation looks like:

function processPayment(method, amount):
    if method == "credit_card":
        // 30 lines of credit card logic
    else if method == "paypal":
        // 25 lines of PayPal logic
    else if method == "crypto":
        // 35 lines of crypto logic
    // Adding a new method? Add another branch here...

Problems with this approach:

  1. Violates the Open/Closed Principle (OCP): Every new algorithm requires modifying the existing function - a change that risks breaking working code.
  2. Violates Single Responsibility Principle (SRP): One class/function knows about every algorithm variant.
  3. Testing difficulty: You cannot unit-test one algorithm in isolation; the entire conditional block must be exercised.
  4. Code bloat: The function grows linearly with each new variant, reducing readability.
  5. Tight coupling: Client code is coupled to every concrete algorithm implementation.

The Strategy Pattern eliminates all five problems by extracting each branch into its own class behind a shared interface.


When to Use / When NOT to Use

When to UseWhen NOT to Use
Multiple algorithms exist for a task and the client should choose at runtimeOnly one algorithm exists and is unlikely to change
You have conditional statements selecting among behavioursThe algorithm is trivial (a few lines) and unlikely to grow
Algorithms use data the client shouldn't know aboutYou need to share significant state between algorithm steps (consider Template Method)
You want to swap algorithms without recompilingThe number of strategies is fixed at compile time and will never grow
You need to isolate algorithm-specific code for testingAdding an interface + classes adds more complexity than the conditional it replaces
Related classes differ only in their behaviourPerformance-critical hot paths where virtual dispatch overhead matters
You want to avoid exposing complex, algorithm-specific data structuresThe "strategies" need to control the flow of the context (consider State)

Key Concepts & Theory

Participants

ParticipantRole
Strategy (interface)Declares the common interface for all supported algorithms
ConcreteStrategyImplements the algorithm using the Strategy interface
ContextMaintains a reference to a Strategy object; delegates algorithm execution to it

Core Principles

  1. Composition over Inheritance: The Context has-a Strategy rather than is-a variant. This avoids deep inheritance hierarchies and allows runtime flexibility.

  2. Program to an Interface: The Context depends only on the Strategy abstraction, never on concrete implementations.

  3. Runtime Algorithm Selection: The strategy can be injected via constructor, setter, or even a factory - enabling dynamic switching based on user input, configuration, or environmental conditions.

  4. Encapsulation of Variation: Each algorithm is encapsulated in its own class, isolating change. Adding a new algorithm means adding a new class - zero modifications to existing code.

  5. Single Responsibility: Each ConcreteStrategy has exactly one reason to change - when its specific algorithm logic changes.

How It Works (Flow)

Client → creates/selects ConcreteStrategy
Client → passes Strategy to Context (via constructor or setter)
Client → calls Context.executeStrategy()
Context → delegates to strategy.execute()
ConcreteStrategy → performs the algorithm, returns result

ASCII Class Diagram

┌─────────────────────────────┐
│          Context            │
├─────────────────────────────┤
│ - strategy: Strategy        │
├─────────────────────────────┤
│ + setStrategy(Strategy)     │
│ + executeStrategy(data)     │──────────┐
└─────────────────────────────┘          │
              │                          │ delegates
              │ holds reference          │
              ▼                          ▼
┌─────────────────────────────┐
│     <<interface>>           │
│        Strategy             │
├─────────────────────────────┤
│ + execute(data): Result     │
└─────────────────────────────┘
              △
              │ implements
    ┌─────────┼─────────────┐
    │         │             │
    ▼         ▼             ▼
┌────────┐ ┌────────┐ ┌────────┐
│Concrete│ │Concrete│ │Concrete│
│Strategy│ │Strategy│ │Strategy│
│   A    │ │   B    │ │   C    │
├────────┤ ├────────┤ ├────────┤
│+execute│ │+execute│ │+execute│
└────────┘ └────────┘ └────────┘

Pseudocode Implementation

Example 1: Payment Processing

// Strategy Interface
interface PaymentStrategy:
    method pay(amount: Money): PaymentResult

// Concrete Strategies
class CreditCardStrategy implements PaymentStrategy:
    cardNumber, expiry, cvv

    method pay(amount):
        validate card details
        connect to card network gateway
        authorize transaction
        return PaymentResult(success, transactionId)

class PayPalStrategy implements PaymentStrategy:
    email, authToken

    method pay(amount):
        authenticate with PayPal API
        create payment intent
        execute payment
        return PaymentResult(success, transactionId)

class CryptoStrategy implements PaymentStrategy:
    walletAddress, network

    method pay(amount):
        convert amount to crypto equivalent
        initiate blockchain transaction
        wait for confirmation (or return pending)
        return PaymentResult(pending, txHash)

// Context
class PaymentContext:
    private strategy: PaymentStrategy

    method setStrategy(strategy: PaymentStrategy):
        this.strategy = strategy

    method checkout(amount: Money): PaymentResult:
        if this.strategy == null:
            throw "No payment strategy configured"
        return this.strategy.pay(amount)

// Client Usage
context = new PaymentContext()

// User selects PayPal at checkout
context.setStrategy(new PayPalStrategy("user@mail.com", token))
result = context.checkout(Money(99.99, "USD"))

// Later, user switches to crypto
context.setStrategy(new CryptoStrategy("0xABC...", "ethereum"))
result = context.checkout(Money(49.99, "USD"))

Example 2: Sorting - Strategy Selected by Data Size

// Strategy Interface
interface SortStrategy:
    method sort(data: List): List

// Concrete Strategies
class BubbleSortStrategy implements SortStrategy:
    method sort(data):
        // O(n²)  -  good for tiny datasets, low overhead
        for i in 0..len(data):
            for j in 0..len(data)-i-1:
                if data[j] > data[j+1]: swap(data, j, j+1)
        return data

class QuickSortStrategy implements SortStrategy:
    method sort(data):
        // O(n log n) avg  -  good for medium datasets
        if len(data) <= 1: return data
        pivot = partition(data)
        return quicksort(left) + [pivot] + quicksort(right)

class MergeSortStrategy implements SortStrategy:
    method sort(data):
        // O(n log n) guaranteed  -  good for large datasets, stable
        if len(data) <= 1: return data
        mid = len(data) / 2
        return merge(sort(data[:mid]), sort(data[mid:]))

// Context with automatic strategy selection
class SortContext:
    private strategy: SortStrategy

    method setStrategy(strategy: SortStrategy):
        this.strategy = strategy

    method autoSelectStrategy(data: List):
        if len(data) < 20:
            this.strategy = new BubbleSortStrategy()
        else if len(data) < 10000:
            this.strategy = new QuickSortStrategy()
        else:
            this.strategy = new MergeSortStrategy()

    method sort(data: List): List:
        this.autoSelectStrategy(data)
        return this.strategy.sort(data)

Note: The autoSelectStrategy method demonstrates that strategy selection can live inside the Context, in a Factory, or in the Client - this is a key design decision (see Constraints section).


Strategy vs State vs Template Method

These three patterns are frequently confused in interviews. Here is a detailed comparison:

DimensionStrategyStateTemplate Method
IntentSelect one algorithm from a familyChange behaviour when internal state changesDefine algorithm skeleton; let subclasses fill in steps
Who triggers changeClient explicitly sets the strategyState transitions happen internally (often automatic)Fixed at compile time (inheritance)
Awareness of siblingsStrategies are unaware of each otherStates often know which state to transition to nextSteps are unaware of alternative implementations
RelationshipContext has-a Strategy (composition)Context has-a State (composition)Base class is-a template (inheritance)
Number activeExactly one strategy at a timeExactly one state at a timeOne fixed skeleton, multiple hook overrides
GranularityReplaces the entire algorithmReplaces all behaviour for current stateReplaces individual steps of an algorithm
Runtime flexibilityHigh - swap anytimeHigh - transitions at runtimeLow - fixed at compile time
Typical exampleSorting algorithm selectionTCP connection (Listen → Established → Closed)Data mining (parse → analyze → report)
OCP complianceNew strategy = new classNew state = new classNew step variant = new subclass
Client knowledgeClient knows about strategies and choosesClient is often unaware of state transitionsClient calls template method, unaware of steps

Key Distinction: Strategy is about choosing an algorithm. State is about becoming something different. Template Method is about customizing steps within a fixed skeleton.


Real-World Examples

DomainContextStrategies
CompressionFileCompressorGzipStrategy, ZipStrategy, Brotli, LZ4
RoutingNavigationServiceShortestPath, FastestRoute, AvoidTolls, ScenicRoute
AuthenticationAuthManagerOAuth2Strategy, SAMLStrategy, LDAPStrategy, APIKeyStrategy
Discount CalculationPricingEnginePercentageDiscount, FlatDiscount, BuyOneGetOne, TieredDiscount
ValidationFormValidatorEmailValidation, PhoneValidation, CreditCardValidation
LoggingLoggerConsoleStrategy, FileStrategy, CloudWatchStrategy
CachingCacheManagerLRUStrategy, LFUStrategy, TTLStrategy
Rate LimitingRateLimiterTokenBucket, SlidingWindow, FixedWindow

Framework Examples

  • Java: Comparator<T> passed to Collections.sort() - the comparator is the strategy.
  • Python: Functions as first-class strategies - sorted(data, key=lambda x: x.age).
  • Spring: AuthenticationStrategy in Spring Security.
  • React: Render props and higher-order components act as strategy injection mechanisms.

Advantages & Disadvantages

AdvantagesDisadvantages
Eliminates conditional statements for algorithm selectionIncreases number of classes/objects in the system
Follows Open/Closed Principle - add strategies without modifying contextClients must be aware of different strategies to select one
Each strategy is independently testableOverhead if strategies are trivial (over-engineering)
Strategies are reusable across multiple contextsCommunication overhead between Context and Strategy (data passing)
Runtime algorithm switching without subclassingAll strategies must conform to the same interface, even if some don't need all parameters
Composition over inheritance - avoids deep hierarchiesStrategy selection logic must live somewhere (moved, not eliminated)
Clean separation of concernsMay require exposing Context internals if strategies need context data

Constraints & Edge Cases

1. Strategy Selection Logic - Who Decides?

The conditional logic doesn't disappear; it moves. Three common approaches:

ApproachProsCons
Client decidesMaximum flexibility, client knows context bestClient coupled to all concrete strategies
Context decides (auto-select)Encapsulates selection logicContext coupled to selection criteria
Factory decidesDecouples both client and context from selectionExtra indirection, factory must be maintained

Recommendation: Use a Factory or configuration map when the number of strategies exceeds 3-4. For simple cases, let the client decide.

2. Strategy with State

Strategies are typically stateless - they receive input, produce output. But some strategies accumulate state (e.g., a retry strategy tracking attempt count). When strategies hold state:

  • They cannot be shared across contexts (thread safety).
  • Consider creating new strategy instances per operation.
  • Document whether a strategy is reusable or single-use.

3. Null Strategy (Default Behaviour)

Instead of null-checking before calling the strategy, implement a NullStrategy (Null Object Pattern):

class NoOpPaymentStrategy implements PaymentStrategy:
    method pay(amount):
        return PaymentResult(skipped, "No payment method configured")

This eliminates defensive null checks in the Context and provides predictable default behaviour.

4. Strategy + Factory Combination

class PaymentStrategyFactory:
    method create(type: String): PaymentStrategy:
        strategies = {
            "credit_card": CreditCardStrategy,
            "paypal": PayPalStrategy,
            "crypto": CryptoStrategy
        }
        return strategies[type].new() or throw UnknownStrategyError

This combination is extremely common in production systems - the Factory handles selection, the Strategy handles execution.

5. Strategy vs Lambda/Function Pointer

In languages with first-class functions, a single-method strategy interface can be replaced by a function reference. Use a full Strategy class when:

  • The algorithm needs multiple methods or configuration.
  • You want to leverage dependency injection frameworks.
  • The strategy carries state or dependencies.

Interview Follow-ups

Q1: How does Strategy Pattern differ from simply using polymorphism with inheritance?

Answer: Inheritance creates a static, compile-time binding - the subclass is a variant forever. Strategy uses composition - the Context has a behaviour that can be swapped at runtime. Inheritance also leads to class explosion when combining multiple varying behaviours (e.g., a Duck that can fly and quack in different ways would need FlyingQuackingDuck, NonFlyingQuackingDuck, etc.). Strategy composes these independently.

Q2: Can a Context have multiple strategies simultaneously?

Answer: Yes. A Context can hold references to multiple strategy interfaces for orthogonal concerns. For example, a ReportGenerator might have a FormattingStrategy (PDF, HTML, CSV) and a DeliveryStrategy (Email, S3, Print) simultaneously. Each strategy axis varies independently. This is sometimes called the "multi-dimensional strategy" approach.

Q3: How do you handle strategy selection in a microservices architecture?

Answer: Strategy selection is often driven by configuration (feature flags, A/B test assignments, or tenant-specific settings). The strategy registry is populated at startup via dependency injection. In distributed systems, the strategy identifier (not the object) is passed across service boundaries, and each service resolves it locally. This keeps services decoupled while allowing coordinated algorithm selection.

Hints-Only Questions

H1: How would you implement a strategy that needs access to the Context's internal state?

Hints:

  • Consider passing the Context itself (this) to the strategy's execute method.
  • Alternatively, define a data-transfer object (DTO) containing only what the strategy needs.
  • Think about the trade-off between coupling and convenience.

H2: How would you unit test a system using the Strategy Pattern?

Hints:

  • Strategies are independently testable - inject mock data, assert output.
  • Test the Context with a mock/stub strategy to verify delegation.
  • Consider property-based testing: all strategies must satisfy the same post-conditions defined by the interface contract.

Counter Questions to Ask Interviewer

  1. "Are the algorithms known at compile time, or do we need to support plugin-style runtime discovery?" - Determines whether a simple factory suffices or you need a registry/service-loader mechanism.

  2. "Do the strategies share any common setup or teardown logic?" - If yes, consider combining Strategy with Template Method (strategy handles the varying part, template handles the skeleton).

  3. "Is the strategy selection made once (at initialization) or does it change during the object's lifetime?" - Affects whether you use constructor injection (immutable) or setter injection (mutable).

  4. "Do strategies need access to shared resources or context state?" - Determines the method signature and whether you pass data explicitly or pass a context reference.

  5. "What are the performance constraints? Is virtual dispatch acceptable in this hot path?" - In extreme performance scenarios, compile-time strategies (templates/generics) may be preferred over runtime polymorphism.


References & Whitepapers

  1. Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: Behavioural Patterns - Strategy. The canonical reference defining the pattern's structure, participants, and consequences.

  2. Freeman, E., Robson, E. - Head First Design Patterns (2004), Chapter 1: Welcome to Design Patterns - The Duck Example. Demonstrates how FlyBehaviour and QuackBehaviour strategies replace inheritance-based duck variants. The most accessible introduction to Strategy.

  3. Martin, R.C. - Agile Software Development: Principles, Patterns, and Practices (2002). Discusses Strategy in the context of SOLID principles, particularly OCP and DIP.

  4. Fowler, M. - Refactoring: Improving the Design of Existing Code (2018). "Replace Conditional with Polymorphism" refactoring - the mechanical process of extracting strategies from conditionals.

  5. Bloch, J. - Effective Java (3rd Edition, 2018), Item 42: Prefer lambdas to anonymous classes. Discusses when functional interfaces replace the Strategy pattern in modern Java.



Strategy is the workhorse pattern of clean architecture. When you see a growing switch statement selecting between algorithms, you're looking at a Strategy Pattern waiting to be extracted. Master it, and you'll find it appearing naturally in almost every system you design.