Strategy Design Pattern: Simplifying Software Design
Stop Writing If-Else Hell: The Case for Strategy
The Strategy Pattern replaces giant if-else or switch blocks with a family of interchangeable algorithm classes.
π― System Scenario & Requirements
System Goal
The Strategy Pattern replaces giant if-else or switch blocks with a family of interchangeable algorithm classes.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: The Strategy Pattern replaces giant if-else or switch blocks with a family of interchangeable algorithm classes.
- Each strategy is a self-contained unit that can be swapped at runtime without touching the client code.
- The result: Open/Closed Principle compliance and dramatically easier testing.
Constraints & Assumptions
- Use Strategy when: - You have multiple algorithms for the same task that differ only in behavior.
- - You need to swap algorithms at runtime based on context or configuration.
β‘ Execution Stages & Decision Trace
Stage 1 β The Three Components of the Strategy Pattern
π Class Hierarchy: Strategy Pattern
π Class Hierarchy: Strategy Pattern
Deep Dive
Scenario context
The Strategy Pattern replaces giant if-else or switch blocks with a family of interchangeable algorithm classes. Each strategy is a self-contained unit that can be swapped at runtime without touching the client code. The result: Open/Closed Principle compliance and dramatically easier testing.
Imagine a PaymentProcessor that handles Stripe, PayPal, Apple Pay, and crypto:
void pay(String type, inβ¦
This walkthrough focuses on **Stop Writing If-Else Hell: The Case for Strategy**.
Before making a design choice, clarify:
- Imagine a PaymentProcessor that handles Stripe, PayPal, Apple Pay, and crypto: The problem: every new payment method forces you to modify the core class, violating the Open/Closed Principle ( open for extension, closedβ¦
- Tests become fragile and the class grows without bound.
- The Strategy Pattern fixes this by extracting each algorithm into its own class.
**Concepts in play:** design-patterns-gof, jvm-internals-and-garbage-collection, graph-traversal-algorithms, api-security-oauth2
## Stage 1 β The Three Components of the Strategy Pattern
- π Class Hierarchy: Strategy Pattern
*Applies [**design-patterns-gof**](/concept/design-patterns-gof) to the decision above.*
```mermaid
graph TD
A[Client: PaymentCheckout] --> B[Context: PaymentProcessor]
B --> C{Strategy interface: PaymentStrategy}
C --> D[StripeStrategy]
C --> E[PayPalStrategy]
C --> F[ApplePayStrategy]
classDiagram
class Context {
-strategy: PaymentStrategy
+setStrategy(s: PaymentStrategy)
+checkout(amount: int)
}
class PaymentStrategy {
<>
+pay(amount: int)
}
class StripeStrategy {
-apiKey: String
+pay(amount: int)
}
class PayPalStrategy {
-email: String
+pay(amount: int)
}
class CryptoStrategy {
-walletAddress: String
+pay(amount: int)
}
Context --> PaymentStrategy
PaymentStrategy <|.. StripeStrategy
PaymentStrategy <|.. PayPalStrategy
PaymentStrategy <|.. CryptoStrategy
Observable outcome: π Class Hierarchy: Strategy Pattern
Stage 2 β Full Implementation: Payment Processing Example
- Adding a new payment method (e.g., ApplePayStrategy ) requires zero changes to PaymentProcessor or any other existing class.
// 1. Strategy interface
interface PaymentStrategy {
void pay(int amount);
}
// 2. Concrete strategies
class StripeStrategy implements PaymentStrategy {
private String apiKey;
public StripeStrategy(String apiKey) { this.apiKey = apiKey; }
@Override
public void pay(int amount) {
System.out.printf("Charging $%d via Stripe (key: %s)%n", amount, apiKey);
// real Stripe API call here
}
}
class PayPalStrategy implements PaymentStrategy {
private String email;
public PayPalStrategy(String email) { this.email = email; }
@Override
public void pay(int amount) {
System.out.printf("Sending $%d to %s via PayPal%n", amount, email);
}
}
// 3. Context
class PaymentProcessor {
private PaymentStrategy strategy;
public PaymentProcessor(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Swap strategy at runtime
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(int amount) {
strategy.pay(amount); // delegate entirely to the strategy
}
}
// 4. Client usage
public class Main {
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor(new StripeStrategy("sk_test_123"));
processor.checkout(99); // β Charging $99 via Stripe
// Runtime swap: user switches to PayPal
processor.setStrategy(new PayPalStrategy("user@example.com"));
processor.checkout(49); // β Sending $49 to user@example.com via PayPal
}
}
Observable outcome: Adding a new payment method (e.g., ApplePayStrategy ) requires zero changes to PaymentProcessor or any other existing class.
Stage 3 β Deep Dive: How Strategy Enables Open/Closed Design
- The Strategy Pattern achieves Open/Closed compliance through composition over inheritance .
- The Context holds an interface reference β not a concrete type β so any conforming object can be injected at runtime.
- This is a direct application of dependency inversion : high-level modules (Context) depend on abstractions (Strategy interface), not on low-level implementations.
- At runtime, the JVM dispatches pay() through a virtual method table β the same call site, different behavior, zero branching logic in the caller.
Observable outcome: At runtime, the JVM dispatches pay() through a virtual method table β the same call site, different behavior, zero branching logic in the caller.
Stage 4 β Runtime Strategy Switching Flow
- The real power of the Strategy Pattern is runtime flexibility β the ability to swap the algorithm in the middle of execution without restarting the application or recompiling a single line of existing code.
- Notice that the PaymentProcessor context class appears exactly once in this diagram and never changes shape.
- All variation lives in the leaf nodes β the concrete strategy objects.
- Adding CryptoStrategy is a new leaf, not a change to the graph.
graph TD
A[Client Code] --> B[Context: PaymentProcessor created with StripeStrategy]
B --> C{User changes payment method?}
C -->|No| D[context.checkout called delegates to StripeStrategy.pay]
C -->|Yes| E[context.setStrategy called with new PayPalStrategy]
E --> F[context.checkout called delegates to PayPalStrategy.pay]
D --> G[Payment result returned]
F --> G
Observable outcome: Adding CryptoStrategy is a new leaf, not a change to the graph.
Stage 5 β Real-World Applications: Where the Strategy Pattern Appears in Real Systems
Observable outcome: You can explain how βReal-World Applications: Where the Strategy Pattern Appears in Real Systemsβ changes the architecture or implementation path.
Stage 6 β What to Learn Next
- Low-Level Design (LLD) for Tic-Tac-Toe - Low-Level Design Guide for Ride Booking - The Ultimate Data Structures Cheat Sheet
Observable outcome: - Low-Level Design (LLD) for Tic-Tac-Toe - Low-Level Design Guide for Ride Booking - The Ultimate Data Structures Cheat Sheet
End-to-end outcome
Imagine a PaymentProcessor that handles Stripe, PayPal, Apple Pay, and crypto: The problem: every new payment method forces you to modify the core class, violating the Open/Closed Principle ( open for extension, closed for modification ).
Alternatives considered
Decision Guide: Strategy vs. Similar Patterns
Decision Guide: Strategy vs. Similar Patterns
Failure and degraded modes
Trade-offs & Failure Modes: When to Use Strategy (and When Not To)
- Detection: Use Strategy when: - You have multiple algorithms for the same task that differ only in behavior.
- User or operator impact: Use Strategy when: - You have multiple algorithms for the same task that differ only in behavior.
