Appearance
Strategy Pattern
Introduction
The Strategy pattern defines a family of algorithms, encapsulates each one behind a common interface, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it. The pattern is the canonical implementation of the Open/Closed Principle: the context class is closed for modification, and new algorithms are added by writing new strategy implementations, never by touching existing code.
Intent
Define a family of algorithms, encapsulate each one, and make them interchangeable so that the algorithm can vary independently from the clients that use it.
Structure
Participants
| Participant | Role |
|---|---|
Strategy (PaymentStrategy) | Common interface for all concrete algorithms. The context depends only on this type. |
ConcreteStrategy (CreditCardStrategy, PayPalStrategy, CryptoStrategy) | Each implements one specific algorithm variant. |
Context (PaymentContext) | Holds a reference to a PaymentStrategy and delegates algorithm execution to it. |
| StrategyRegistry | Maps type keys to strategy instances; used when selection must happen at runtime from an external discriminator (e.g., a string from a request). |
Anti-Pattern — Switch Statement Payment Processor
The most common trigger for Strategy is a switch or if/else ladder that selects an algorithm based on a type discriminator. Each new payment type means reopening the class, adding another branch, and re-running every existing test.
java
// ANTI-PATTERN: switch statement grows without bound
public class PaymentProcessor {
public PaymentResult process(String type, BigDecimal amount, String customerId) {
switch (type) {
case "CREDIT_CARD":
// validate card, call Stripe, await webhook
String chargeId = callStripeApi(amount, customerId);
return PaymentResult.success(chargeId);
case "PAYPAL":
// redirect flow, await OAuth callback, execute payment
String paypalToken = initiatePayPalOAuth(customerId);
String txId = executePayPalPayment(paypalToken, amount);
return PaymentResult.success(txId);
case "CRYPTO":
// generate address, poll blockchain, await confirmations
String address = generateCryptoAddress();
awaitBlockchainConfirmation(address, amount);
return PaymentResult.success(address);
// Adding Apple Pay means opening this class, adding a case,
// and risking regression in credit card and PayPal logic.
default:
throw new IllegalArgumentException("Unknown payment type: " + type);
}
}
// All private helpers live here, interleaved and impossible to test in isolation
private String callStripeApi(BigDecimal amount, String customerId) { return "ch_xxx"; }
private String initiatePayPalOAuth(String customerId) { return "token_xxx"; }
private String executePayPalPayment(String token, BigDecimal amount) { return "tx_xxx"; }
private String generateCryptoAddress() { return "0x..."; }
private void awaitBlockchainConfirmation(String address, BigDecimal amount) {}
}Problems:
PaymentProcessorviolates OCP: it must be modified for every new payment type.- It violates SRP: it owns the implementation logic for every payment method simultaneously.
- Unit testing credit card logic requires constructing an object that contains PayPal and crypto logic.
- The
defaultthrow makes adding a new type a runtime discovery rather than a compile-time contract.
Correct Implementation
Strategy Interface and Result
java
import java.math.BigDecimal;
public record PaymentResult(boolean success, String transactionId, String errorMessage) {
public static PaymentResult success(String transactionId) {
return new PaymentResult(true, transactionId, null);
}
public static PaymentResult failure(String reason) {
return new PaymentResult(false, null, reason);
}
}java
public interface PaymentStrategy {
PaymentResult process(BigDecimal amount, String customerId);
boolean supports(String paymentType);
}Concrete Strategies
java
public class CreditCardStrategy implements PaymentStrategy {
private final String cardToken;
private final StripeGateway stripeGateway;
public CreditCardStrategy(String cardToken, StripeGateway stripeGateway) {
this.cardToken = cardToken;
this.stripeGateway = stripeGateway;
}
@Override
public PaymentResult process(BigDecimal amount, String customerId) {
try {
String chargeId = stripeGateway.charge(cardToken, amount, customerId);
System.out.printf("[CreditCard] Charged $%s to token %s — charge %s%n",
amount, cardToken, chargeId);
return PaymentResult.success(chargeId);
} catch (GatewayException ex) {
return PaymentResult.failure("Gateway declined: " + ex.getMessage());
}
}
@Override
public boolean supports(String paymentType) {
return "CREDIT_CARD".equalsIgnoreCase(paymentType);
}
}java
public class PayPalStrategy implements PaymentStrategy {
private final String clientId;
private final PayPalClient payPalClient;
public PayPalStrategy(String clientId, PayPalClient payPalClient) {
this.clientId = clientId;
this.payPalClient = payPalClient;
}
@Override
public PaymentResult process(BigDecimal amount, String customerId) {
try {
String token = payPalClient.createOrder(amount, customerId);
String txId = payPalClient.captureOrder(token);
System.out.printf("[PayPal] Captured $%s — tx %s%n", amount, txId);
return PaymentResult.success(txId);
} catch (PayPalException ex) {
return PaymentResult.failure("PayPal error: " + ex.getMessage());
}
}
@Override
public boolean supports(String paymentType) {
return "PAYPAL".equalsIgnoreCase(paymentType);
}
}java
public class CryptoStrategy implements PaymentStrategy {
private final String walletAddress;
private final BlockchainClient blockchainClient;
public CryptoStrategy(String walletAddress, BlockchainClient blockchainClient) {
this.walletAddress = walletAddress;
this.blockchainClient = blockchainClient;
}
@Override
public PaymentResult process(BigDecimal amount, String customerId) {
String txHash = blockchainClient.initiateTransfer(walletAddress, amount);
blockchainClient.awaitConfirmations(txHash, 3); // wait for 3 block confirmations
System.out.printf("[Crypto] Transfer confirmed — tx %s%n", txHash);
return PaymentResult.success(txHash);
}
@Override
public boolean supports(String paymentType) {
return "CRYPTO".equalsIgnoreCase(paymentType);
}
}Context
The context owns a reference to the active strategy and delegates to it. The context is never modified when new strategies are introduced.
java
public class PaymentContext {
private PaymentStrategy strategy;
// Constructor injection: strategy is fixed for the lifetime of this context
public PaymentContext(PaymentStrategy strategy) {
this.strategy = strategy;
}
// Setter injection: strategy can be swapped at runtime
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public PaymentResult executePayment(BigDecimal amount, String customerId) {
if (strategy == null) {
throw new IllegalStateException("No payment strategy configured");
}
return strategy.process(amount, customerId);
}
}Strategy Registry — Selecting by Type at Runtime
When the payment type arrives as a string from an HTTP request or message queue, a registry provides O(1) lookup without any conditional logic in the context.
java
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PaymentStrategyRegistry {
private final Map<String, PaymentStrategy> registry = new HashMap<>();
// Spring can inject all PaymentStrategy beans as a List automatically
public PaymentStrategyRegistry(List<PaymentStrategy> strategies) {
for (PaymentStrategy strategy : strategies) {
// Each strategy declares what type key it handles via supports()
// Register against a canonical type name
registry.put(strategy.getClass().getSimpleName(), strategy);
}
}
// Explicit registration for clarity
public void register(String type, PaymentStrategy strategy) {
registry.put(type.toUpperCase(), strategy);
}
public PaymentStrategy resolve(String paymentType) {
PaymentStrategy strategy = registry.get(paymentType.toUpperCase());
if (strategy == null) {
throw new UnsupportedOperationException(
"No payment strategy registered for type: " + paymentType
);
}
return strategy;
}
}java
// Usage — registry-based selection (no switch statement anywhere)
public class CheckoutService {
private final PaymentStrategyRegistry registry;
public CheckoutService(PaymentStrategyRegistry registry) {
this.registry = registry;
}
public PaymentResult checkout(String paymentType, BigDecimal amount, String customerId) {
PaymentStrategy strategy = registry.resolve(paymentType);
PaymentContext context = new PaymentContext(strategy);
return context.executePayment(amount, customerId);
}
}Interaction Flow
Real-World Examples
java.util.Comparator
Comparator<T> is Strategy. Collections.sort() and List.sort() accept a comparator that encapsulates the comparison algorithm, keeping the sort mechanics in the JDK untouched.
java
List<Order> orders = new ArrayList<>(fetchOrders());
// Strategy 1: sort by amount descending
orders.sort(Comparator.comparing(Order::getAmount).reversed());
// Strategy 2: sort by date then customer name
orders.sort(Comparator.comparing(Order::getCreatedAt)
.thenComparing(Order::getCustomerName));
// Strategy 3: entirely custom — highest priority first, then oldest
orders.sort((a, b) -> {
int priorityCmp = Integer.compare(b.getPriority(), a.getPriority());
return priorityCmp != 0 ? priorityCmp : a.getCreatedAt().compareTo(b.getCreatedAt());
});Spring Security AuthenticationStrategy
Spring Security's SessionAuthenticationStrategy is a Strategy interface. The framework selects among ChangeSessionIdAuthenticationStrategy, ConcurrentSessionControlAuthenticationStrategy, or CompositeSessionAuthenticationStrategy without any switch logic in the core authentication flow.
AWS SDK Retry Policy
The AWS SDK v2 defines RetryPolicy as a Strategy. Applications supply a custom RetryCondition and BackoffStrategy without modifying SDK internals.
java
import software.amazon.awssdk.core.retry.RetryPolicy;
import software.amazon.awssdk.core.retry.backoff.FixedDelayBackoffStrategy;
import software.amazon.awssdk.core.retry.conditions.RetryCondition;
RetryPolicy customPolicy = RetryPolicy.builder()
.numRetries(5)
.retryCondition(RetryCondition.defaultRetryCondition())
.backoffStrategy(FixedDelayBackoffStrategy.create(Duration.ofMillis(500)))
.build();
S3Client s3 = S3Client.builder()
.overrideConfiguration(c -> c.retryPolicy(customPolicy))
.build();Pricing Algorithm Selection
E-commerce platforms commonly swap pricing strategies based on customer segment, geography, or promotional campaign — classic runtime Strategy selection via a registry.
java
public interface PricingStrategy {
BigDecimal calculatePrice(BigDecimal basePrice, String customerId);
}
@Component("STANDARD") class StandardPricing implements PricingStrategy { /* ... */ }
@Component("PREMIUM") class PremiumPricing implements PricingStrategy { /* ... */ }
@Component("WHOLESALE") class WholesalePricing implements PricingStrategy { /* ... */ }When to Use
- Multiple related classes differ only in behavior; Strategy allows configuring a class with one of many behaviors.
- You need different variants of an algorithm and want to avoid exposing algorithm-specific data structures.
- A class defines many behaviors that appear as multiple conditional statements; move each branch into its own strategy.
- The algorithm selection must happen at runtime based on user input, configuration, or context.
Consequences
Benefits
- Eliminates conditional statements: the client calls
strategy.execute()regardless of which algorithm is active. - Strategies are independently testable in isolation — each concrete class has its own unit test.
- New algorithms are added by writing new classes; existing strategies and the context are untouched (OCP).
- Strategies can be hot-swapped at runtime via
setStrategy().
Trade-offs
- Clients must be aware of different strategies to select the right one (mitigated by a registry or factory).
- Increased object count: each strategy is a separate class. For trivial algorithm variants, this may feel like over-engineering.
- Context and strategy share no implicit state — all data the strategy needs must be passed explicitly, which can bloat the
process()signature.
Relationship to OCP and DIP
Strategy is the canonical OCP implementation: the context is closed for modification, and each new algorithm extends the system by adding a new class. It also embodies the Dependency Inversion Principle: the high-level PaymentContext depends on the PaymentStrategy abstraction, not on any concrete payment implementation.
Related Concepts
- Open/Closed Principle: Strategy is the primary mechanism for achieving OCP in object-oriented code. The OCP article contains a parallel payment example that shows the same pattern from a principle-first perspective.
- Dependency Inversion Principle: The context must depend on the strategy interface (abstraction), not on any concrete algorithm — DIP is what makes the decoupling work.
- Template Method Pattern: Template Method achieves a similar goal through inheritance rather than composition. Use Strategy when the algorithm must be swappable at runtime; use Template Method when the skeleton is fixed at compile time. The two are sometimes combined: a template method calls abstract steps that are implemented via injected strategies.
- Observer Pattern: Observer decouples event producers from event consumers; Strategy decouples algorithm selection from algorithm execution. An observer callback can itself be implemented as a strategy to make its reaction configurable.
- Saga Pattern: Saga orchestrators often use Strategy to select compensation or retry policies per step type.