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:
- Violates the Open/Closed Principle (OCP): Every new algorithm requires modifying the existing function - a change that risks breaking working code.
- Violates Single Responsibility Principle (SRP): One class/function knows about every algorithm variant.
- Testing difficulty: You cannot unit-test one algorithm in isolation; the entire conditional block must be exercised.
- Code bloat: The function grows linearly with each new variant, reducing readability.
- 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 Use | When NOT to Use |
|---|---|
| Multiple algorithms exist for a task and the client should choose at runtime | Only one algorithm exists and is unlikely to change |
| You have conditional statements selecting among behaviours | The algorithm is trivial (a few lines) and unlikely to grow |
| Algorithms use data the client shouldn't know about | You need to share significant state between algorithm steps (consider Template Method) |
| You want to swap algorithms without recompiling | The number of strategies is fixed at compile time and will never grow |
| You need to isolate algorithm-specific code for testing | Adding an interface + classes adds more complexity than the conditional it replaces |
| Related classes differ only in their behaviour | Performance-critical hot paths where virtual dispatch overhead matters |
| You want to avoid exposing complex, algorithm-specific data structures | The "strategies" need to control the flow of the context (consider State) |
Key Concepts & Theory
Participants
| Participant | Role |
|---|---|
| Strategy (interface) | Declares the common interface for all supported algorithms |
| ConcreteStrategy | Implements the algorithm using the Strategy interface |
| Context | Maintains a reference to a Strategy object; delegates algorithm execution to it |
Core Principles
-
Composition over Inheritance: The Context has-a Strategy rather than is-a variant. This avoids deep inheritance hierarchies and allows runtime flexibility.
-
Program to an Interface: The Context depends only on the Strategy abstraction, never on concrete implementations.
-
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.
-
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.
-
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
autoSelectStrategymethod 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:
| Dimension | Strategy | State | Template Method |
|---|---|---|---|
| Intent | Select one algorithm from a family | Change behaviour when internal state changes | Define algorithm skeleton; let subclasses fill in steps |
| Who triggers change | Client explicitly sets the strategy | State transitions happen internally (often automatic) | Fixed at compile time (inheritance) |
| Awareness of siblings | Strategies are unaware of each other | States often know which state to transition to next | Steps are unaware of alternative implementations |
| Relationship | Context has-a Strategy (composition) | Context has-a State (composition) | Base class is-a template (inheritance) |
| Number active | Exactly one strategy at a time | Exactly one state at a time | One fixed skeleton, multiple hook overrides |
| Granularity | Replaces the entire algorithm | Replaces all behaviour for current state | Replaces individual steps of an algorithm |
| Runtime flexibility | High - swap anytime | High - transitions at runtime | Low - fixed at compile time |
| Typical example | Sorting algorithm selection | TCP connection (Listen → Established → Closed) | Data mining (parse → analyze → report) |
| OCP compliance | New strategy = new class | New state = new class | New step variant = new subclass |
| Client knowledge | Client knows about strategies and chooses | Client is often unaware of state transitions | Client 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
| Domain | Context | Strategies |
|---|---|---|
| Compression | FileCompressor | GzipStrategy, ZipStrategy, Brotli, LZ4 |
| Routing | NavigationService | ShortestPath, FastestRoute, AvoidTolls, ScenicRoute |
| Authentication | AuthManager | OAuth2Strategy, SAMLStrategy, LDAPStrategy, APIKeyStrategy |
| Discount Calculation | PricingEngine | PercentageDiscount, FlatDiscount, BuyOneGetOne, TieredDiscount |
| Validation | FormValidator | EmailValidation, PhoneValidation, CreditCardValidation |
| Logging | Logger | ConsoleStrategy, FileStrategy, CloudWatchStrategy |
| Caching | CacheManager | LRUStrategy, LFUStrategy, TTLStrategy |
| Rate Limiting | RateLimiter | TokenBucket, SlidingWindow, FixedWindow |
Framework Examples
- Java:
Comparator<T>passed toCollections.sort()- the comparator is the strategy. - Python: Functions as first-class strategies -
sorted(data, key=lambda x: x.age). - Spring:
AuthenticationStrategyin Spring Security. - React: Render props and higher-order components act as strategy injection mechanisms.
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Eliminates conditional statements for algorithm selection | Increases number of classes/objects in the system |
| Follows Open/Closed Principle - add strategies without modifying context | Clients must be aware of different strategies to select one |
| Each strategy is independently testable | Overhead if strategies are trivial (over-engineering) |
| Strategies are reusable across multiple contexts | Communication overhead between Context and Strategy (data passing) |
| Runtime algorithm switching without subclassing | All strategies must conform to the same interface, even if some don't need all parameters |
| Composition over inheritance - avoids deep hierarchies | Strategy selection logic must live somewhere (moved, not eliminated) |
| Clean separation of concerns | May 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:
| Approach | Pros | Cons |
|---|---|---|
| Client decides | Maximum flexibility, client knows context best | Client coupled to all concrete strategies |
| Context decides (auto-select) | Encapsulates selection logic | Context coupled to selection criteria |
| Factory decides | Decouples both client and context from selection | Extra 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
-
"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.
-
"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).
-
"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).
-
"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.
-
"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
-
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.
-
Freeman, E., Robson, E. - Head First Design Patterns (2004), Chapter 1: Welcome to Design Patterns - The Duck Example. Demonstrates how
FlyBehaviourandQuackBehaviourstrategies replace inheritance-based duck variants. The most accessible introduction to Strategy. -
Martin, R.C. - Agile Software Development: Principles, Patterns, and Practices (2002). Discusses Strategy in the context of SOLID principles, particularly OCP and DIP.
-
Fowler, M. - Refactoring: Improving the Design of Existing Code (2018). "Replace Conditional with Polymorphism" refactoring - the mechanical process of extracting strategies from conditionals.
-
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.
Related Topics
- Observer Pattern - Often combined with Strategy to notify when strategy changes
- Factory Method Pattern - Used to create/select the appropriate strategy
- State Pattern - Structurally identical but semantically different (behaviour changes with state transitions)
- Template Method Pattern - Uses inheritance instead of composition for algorithm variation
- Command Pattern - Encapsulates a request as an object; Strategy encapsulates an algorithm
- Decorator Pattern - Changes the skin (wraps); Strategy changes the guts (replaces)
- Dependency Injection - The mechanism by which strategies are typically provided to contexts
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.