Skip to content

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

ParticipantRole
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.
StrategyRegistryMaps 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:

  • PaymentProcessor violates 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 default throw 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.

  • 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.