Strategy Design Pattern: Simplifying Software Design
Stop writing massive if-else statements. The Strategy Pattern allows you to swap algorithms at runtime. We explain it with a Payment Processing exampl
Abstract Algorithms
AI-assisted content. This post may have been written or enhanced with AI tools. Please verify critical information independently.
TLDR: The Strategy Pattern replaces giant
if-elseorswitchblocks 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.
๐ Stop Writing If-Else Hell: The Case for Strategy
Imagine a PaymentProcessor that handles Stripe, PayPal, Apple Pay, and crypto:
void pay(String type, int amount) {
if (type.equals("STRIPE")) {
// 50 lines of Stripe logic
} else if (type.equals("PAYPAL")) {
// 50 lines of PayPal logic
} else if (type.equals("BITCOIN")) {
// 50 lines of Bitcoin logic
}
// Adding "Apple Pay" means modifying this class โ wrong
}
The problem: every new payment method forces you to modify the core class, violating the Open/Closed Principle (open for extension, closed for modification). Tests become fragile and the class grows without bound.
The Strategy Pattern fixes this by extracting each algorithm into its own class.
๐ The Three Components of the Strategy Pattern
graph TD
A[Client: PaymentCheckout] --> B[Context: PaymentProcessor]
B --> C{Strategy interface: PaymentStrategy}
C --> D[StripeStrategy]
C --> E[PayPalStrategy]
C --> F[ApplePayStrategy]
| Component | Role | Example |
| Strategy (interface) | Defines the contract all algorithms must follow | PaymentStrategy.pay(int amount) |
| Concrete Strategy | Implements the algorithm | StripeStrategy, PayPalStrategy |
| Context | Holds a reference to the current strategy; delegates work to it | PaymentProcessor |
๐ Class Hierarchy: Strategy Pattern
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
โ๏ธ Full Implementation: Payment Processing Example
// 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
}
}
Adding a new payment method (e.g., ApplePayStrategy) requires zero changes to PaymentProcessor or any other existing class.
๐ง 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.
๐ 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.
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
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.
When does runtime swapping occur in practice?
- A user changes their preferred payment method during checkout without reloading the page.
- A configuration flag switches the log-routing strategy per deployment environment (
FileLoggerin dev,CloudWatchLoggerin production). - A circuit breaker detects the primary pricing service is slow and activates a cached fallback pricing strategy.
- A nightly batch job switches the compression algorithm from speed-optimized (LZ4) to size-optimized (Zstd) during off-peak hours.
The pattern scales cleanly because every new use case only adds a new class โ it never mutates the infrastructure that calls it. This is why large codebases often contain dozens of strategy implementations for a single context: each one handles exactly one case, is tested independently, and never risks breaking the others.
๐ Real-World Applications: Where the Strategy Pattern Appears in Real Systems
| Domain | Strategy family | What changes at runtime |
| Sorting library | QuickSort, MergeSort, HeapSort | Algorithm chosen by input size or stability requirement |
| Compression | GzipStrategy, ZstdStrategy, LZ4Strategy | Chosen by latency vs. ratio trade-off |
| Authentication | JWTStrategy, OAuth2Strategy, ApiKeyStrategy | Chosen per API endpoint config |
| Pricing engine | PercentDiscountStrategy, FlatRateStrategy, TieredPricingStrategy | Chosen by customer tier |
| Log routing | FileLogger, CloudWatchLogger, StdoutLogger | Chosen by environment config |
โ๏ธ Trade-offs & Failure Modes: When to Use Strategy (and When Not To)
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.
- You want to eliminate conditional logic that grows with each new algorithm.
- Each algorithm variant needs to be independently unit-testable.
Avoid Strategy when:
- You only have two variants that never change โ a simple
ifis cleaner. - The number of strategies is likely to stay at one โ the abstraction has no payoff.
- Performance is critical and the interface dispatch overhead is measurable.
| Trade-off | Detail |
| + OCP compliance | New strategies extend without modifying existing code |
| + Testability | Each strategy is a self-contained class with focused tests |
| + Runtime flexibility | Swap behavior without restarting or redeploying |
| โ Strategy count | Too many small strategy classes can clutter the package |
| โ Indirection | Client must know which strategies exist and how to configure them |
๐งญ Decision Guide: Strategy vs. Similar Patterns
| Pattern | Key difference |
| Strategy | Swaps algorithms for the same task at runtime |
| Template Method | Defines a skeleton algorithm in a base class; subclasses fill in specific steps |
| State | Swaps behavior based on internal state transitions (not just algorithm) |
| Command | Encapsulates an action (not just an algorithm) as an object with undo support |
๐ฏ 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
๐งช Hands-On: Add a Crypto Payment Strategy
The best way to verify you understand the pattern is to extend the existing system without modifying any code you did not write. Follow these steps:
Task: Add a CryptoStrategy that accepts a wallet address and prints a simulated blockchain payment.
Step 1 โ Write the strategy class:
class CryptoStrategy implements PaymentStrategy {
private String walletAddress;
public CryptoStrategy(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(int amount) {
System.out.printf("Sending %d USDC to wallet %s on-chain%n", amount, walletAddress);
// real blockchain call here
}
}
Step 2 โ Wire it into the client:
processor.setStrategy(new CryptoStrategy("0xAbCd...1234"));
processor.checkout(200);
// โ Sending 200 USDC to wallet 0xAbCd...1234 on-chain
Step 3 โ Verify the rule: Open PaymentProcessor.java. Confirm you made zero edits to it. You only added a new file. That is the Open/Closed Principle in action.
Challenge: Now add a SubscriptionStrategy that checks if the user has a monthly cap remaining before charging. Write a unit test that uses a mock PaymentStrategy to assert pay() is called exactly once with the correct amount. Notice how easy the mock is to write because PaymentStrategy is a single-method interface.
๐ ๏ธ Spring Boot: Wiring Strategy Beans with @Service and @Qualifier
Spring Boot's dependency injection is the production-grade way to implement the Strategy Pattern in Java โ instead of manually calling new StripeStrategy(...), you declare each strategy as a @Service bean and let Spring inject the correct one via @Qualifier or a Map<String, PaymentStrategy> at startup. This moves algorithm selection entirely into configuration, making strategies swappable without recompiling a single line of business logic.
Spring solves the lesson from this post's Lesson 2: inject strategies via dependency injection, not hard-coded new. Operators can switch the active strategy by changing a config value; the PaymentProcessor context class never sees a new keyword.
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
// โโ 1. Strategy interface โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public interface PaymentStrategy {
void pay(int amount);
String name(); // used as the bean qualifier key
}
// โโ 2. Strategy beans: each is an independent @Service โโโโโโโโโโโโโโโโโโโโโโโ
@Service("stripe")
public class StripeStrategy implements PaymentStrategy {
public void pay(int amount) {
System.out.printf("Stripe: charging $%d via API%n", amount);
}
public String name() { return "stripe"; }
}
@Service("paypal")
public class PayPalStrategy implements PaymentStrategy {
public void pay(int amount) {
System.out.printf("PayPal: sending $%d%n", amount);
}
public String name() { return "paypal"; }
}
@Service("crypto")
public class CryptoStrategy implements PaymentStrategy {
public void pay(int amount) {
System.out.printf("Crypto: broadcasting %d USDC on-chain%n", amount);
}
public String name() { return "crypto"; }
}
// โโ 3. Context: Spring injects ALL strategies into a Map keyed by bean name โโโ
@Service
public class PaymentProcessor {
// Spring auto-populates this map: {"stripe" โ StripeStrategy, "paypal" โ ..., ...}
private final Map<String, PaymentStrategy> strategies;
@Autowired
public PaymentProcessor(Map<String, PaymentStrategy> strategies) {
this.strategies = strategies;
}
public void checkout(String method, int amount) {
PaymentStrategy strategy = strategies.get(method);
if (strategy == null) {
throw new IllegalArgumentException("Unknown payment method: " + method);
}
strategy.pay(amount); // zero if-else, zero switch
}
}
// โโ 4. REST controller that drives the context โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/checkout")
public class CheckoutController {
private final PaymentProcessor processor;
public CheckoutController(PaymentProcessor processor) {
this.processor = processor;
}
// POST /api/checkout?method=stripe&amount=99
@PostMapping
public String checkout(@RequestParam String method, @RequestParam int amount) {
processor.checkout(method, amount);
return "Payment of $" + amount + " via " + method + " processed.";
}
}
Adding a new payment method (e.g., ApplePayStrategy) requires creating one new @Service("applepay") class โ zero changes to PaymentProcessor, CheckoutController, or any existing strategy. This is exactly what the Open/Closed Principle demands.
For a full deep-dive on Spring dependency injection patterns and @Service/@Qualifier wiring, a dedicated follow-up post is planned.
๐ Lessons from Real-World Strategy Implementations
Applying the Strategy Pattern across production systems reveals a consistent set of lessons that are not obvious from toy examples.
Lesson 1 โ Name strategies by their algorithm, not their caller. StripePaymentStrategy is weaker than StripeStrategy. The caller context (Payment) belongs in the package name, not the class name. When you later reuse StripeStrategy for subscription billing, you'll be glad you named it cleanly.
Lesson 2 โ Inject strategies via dependency injection, not hard-coded new. In Spring, Guice, or CDI, strategies are beans. A @Qualifier annotation selects the correct one at boot time. This moves algorithm selection from code to configuration โ operators can switch strategies by changing a config value, not a deployment.
Lesson 3 โ Avoid stateful strategies. Each strategy instance should be stateless and thread-safe so the same object can be shared across concurrent requests. If a strategy must carry state (e.g., an API key or a rate limiter), use the constructor to inject it and make all fields final.
Lesson 4 โ Combine with Factory for cleaner client code. A PaymentStrategyFactory.forType(String type) method encapsulates the if-else that selects a strategy. This concentrates all algorithm-selection logic in one place, leaving the Context completely clean.
Lesson 5 โ The pattern reveals bad design elsewhere. If you find it hard to extract a strategy because its logic is intertwined with unrelated state, that is a Single Responsibility Principle violation in disguise โ the class is doing too many things at once. Refactoring to Strategy forces you to separate algorithm logic from the data it operates on, which is always the right direction.
๐ TLDR: Summary & Key Takeaways
- Strategy encapsulates interchangeable algorithms behind a common interface, enabling runtime swapping.
- The Context class holds a reference to the current strategy โ it delegates entirely rather than implementing logic.
- Adding a new strategy never requires modifying existing classes โ a direct implementation of the Open/Closed Principle.
- Strategy excels at replacing
if-elsegrowth patterns in algorithm-selection logic. - Each concrete strategy is independently unit-testable โ a major maintainability win.
๐ Related Posts
- Low-Level Design Guide for Ride Booking Application
- Open/Closed Principle Explained
- Single Responsibility Principle Explained
Test Your Knowledge
Ready to test what you just learned?
AI will generate 4 questions based on this article's content.

Written by
Abstract Algorithms
@abstractalgorithms
More Posts
RAG vs Fine-Tuning: When to Use Each (and When to Combine Them)
TLDR: RAG gives LLMs access to current knowledge at inference time; fine-tuning changes how they reason and write. Use RAG when your data changes. Use fine-tuning when you need consistent style, tone, or domain reasoning. Use both for production assi...
Fine-Tuning LLMs with LoRA and QLoRA: A Practical Deep-Dive
TLDR: LoRA freezes the base model and trains two tiny matrices per layer โ 0.1 % of parameters, 70 % less GPU memory, near-identical quality. QLoRA adds 4-bit NF4 quantization of the frozen base, enabling 70B fine-tuning on 2ร A100 80 GB instead of 8...
Build vs Buy: Deploying Your Own LLM vs Using ChatGPT, Gemini, and Claude APIs
TLDR: Use the API until you hit $10K/month or a hard data privacy requirement. Then add a semantic cache. Then evaluate hybrid routing. Self-hosting full model serving is only cost-effective at > 50M tokens/day with a dedicated MLOps team. The build ...
Watermarking and Late Data Handling in Spark Structured Streaming
TLDR: A watermark tells Spark Structured Streaming: "I will accept events up to N minutes late, and then I am done waiting." Spark tracks the maximum event time seen per partition, takes the global minimum across all partitions, subtracts the thresho...
