You’re building a payment system. Credit cards need fraud checks. PayPal requires API authentication. Crypto payments involve blockchain verification. Bank transfers have different validation rules.

You could write a giant if-else chain. Or you could use Strategy.

What is the Strategy Pattern?

Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The client code works with a strategy interface. The actual algorithm can be swapped at runtime without changing the client.

Strategy Design Pattern class diagram showing Payment context with PayMethod interface and Card, PayPal, Crypto interchangeable strategies

Payment doesn’t know which method it’s using (Card, PayPal, or Crypto). It just knows the PayMethod interface. You can add new payment methods without touching existing code.

When to Use Strategy

Use Strategy When Avoid Strategy When
You have multiple algorithms for the same task Only one algorithm exists and won’t change
Algorithms should be interchangeable at runtime The algorithm is simple and inline
You want to isolate algorithm-specific code Adding abstraction would complicate things
Conditional statements select between algorithms Strategies would be nearly identical

If you see code like if (type == "A") doA() else if (type == "B") doB() where each branch has substantial logic, that’s a strategy waiting to emerge.

Implementation

Let’s build a payment processing system with multiple payment methods.

Step 1: Define the Strategy Interface

1
2
3
4
5
public interface PaymentStrategy {
    boolean validate(PaymentDetails details);
    PaymentResult process(PaymentDetails details);
    String getPaymentMethodName();
}

Step 2: Implement Concrete Strategies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
public class CreditCardStrategy implements PaymentStrategy {
    private final FraudDetectionService fraudService;
    private final PaymentGateway gateway;
    
    public CreditCardStrategy(FraudDetectionService fraudService, PaymentGateway gateway) {
        this.fraudService = fraudService;
        this.gateway = gateway;
    }
    
    @Override
    public boolean validate(PaymentDetails details) {
        // Card-specific validation
        if (!isValidCardNumber(details.getCardNumber())) {
            return false;
        }
        if (!isValidExpiry(details.getExpiry())) {
            return false;
        }
        // Check CVV
        return details.getCvv() != null && details.getCvv().length() == 3;
    }
    
    @Override
    public PaymentResult process(PaymentDetails details) {
        // Fraud check first
        if (fraudService.isSuspicious(details)) {
            return PaymentResult.declined("Fraud check failed");
        }
        
        // Process through gateway
        GatewayResponse response = gateway.charge(
            details.getCardNumber(),
            details.getAmount(),
            details.getCurrency()
        );
        
        return response.isSuccess() 
            ? PaymentResult.success(response.getTransactionId())
            : PaymentResult.declined(response.getErrorMessage());
    }
    
    @Override
    public String getPaymentMethodName() {
        return "Credit Card";
    }
    
    private boolean isValidCardNumber(String number) {
        // Luhn algorithm check
        return number != null && number.matches("\\d{16}");
    }
    
    private boolean isValidExpiry(String expiry) {
        // MM/YY format, not expired
        return expiry != null && expiry.matches("\\d{2}/\\d{2}");
    }
}

public class PayPalStrategy implements PaymentStrategy {
    private final PayPalClient paypalClient;
    
    public PayPalStrategy(PayPalClient paypalClient) {
        this.paypalClient = paypalClient;
    }
    
    @Override
    public boolean validate(PaymentDetails details) {
        // PayPal needs email
        return details.getEmail() != null && details.getEmail().contains("@");
    }
    
    @Override
    public PaymentResult process(PaymentDetails details) {
        // Create PayPal payment
        PayPalPayment payment = paypalClient.createPayment(
            details.getAmount(),
            details.getCurrency(),
            details.getEmail()
        );
        
        // Execute payment
        PayPalResponse response = paypalClient.executePayment(payment.getId());
        
        return response.getState().equals("approved")
            ? PaymentResult.success(response.getTransactionId())
            : PaymentResult.declined("PayPal payment not approved");
    }
    
    @Override
    public String getPaymentMethodName() {
        return "PayPal";
    }
}

public class CryptoStrategy implements PaymentStrategy {
    private final BlockchainService blockchain;
    private final ExchangeRateService exchangeService;
    
    public CryptoStrategy(BlockchainService blockchain, ExchangeRateService exchangeService) {
        this.blockchain = blockchain;
        this.exchangeService = exchangeService;
    }
    
    @Override
    public boolean validate(PaymentDetails details) {
        // Validate wallet address format
        String wallet = details.getCryptoWallet();
        return wallet != null && blockchain.isValidAddress(wallet);
    }
    
    @Override
    public PaymentResult process(PaymentDetails details) {
        // Convert amount to crypto
        BigDecimal cryptoAmount = exchangeService.convert(
            details.getAmount(),
            details.getCurrency(),
            details.getCryptoType()
        );
        
        // Create and broadcast transaction
        Transaction tx = blockchain.createTransaction(
            details.getCryptoWallet(),
            cryptoAmount
        );
        
        String txHash = blockchain.broadcast(tx);
        
        // Wait for confirmation
        boolean confirmed = blockchain.waitForConfirmation(txHash, 3);
        
        return confirmed
            ? PaymentResult.success(txHash)
            : PaymentResult.pending("Awaiting blockchain confirmation");
    }
    
    @Override
    public String getPaymentMethodName() {
        return "Cryptocurrency";
    }
}

Step 3: Create the Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PaymentProcessor {
    private PaymentStrategy strategy;
    private final PaymentLogger logger;
    
    public PaymentProcessor(PaymentLogger logger) {
        this.logger = logger;
    }
    
    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
        logger.log("Payment method set to: " + strategy.getPaymentMethodName());
    }
    
    public PaymentResult processPayment(PaymentDetails details) {
        if (strategy == null) {
            throw new IllegalStateException("Payment strategy not set");
        }
        
        logger.log("Processing " + strategy.getPaymentMethodName() + " payment");
        
        // Validate first
        if (!strategy.validate(details)) {
            logger.log("Validation failed");
            return PaymentResult.invalid("Payment details validation failed");
        }
        
        // Process payment
        PaymentResult result = strategy.process(details);
        
        logger.log("Payment result: " + result.getStatus());
        return result;
    }
}

Step 4: Use the Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CheckoutService {
    private final PaymentProcessor processor;
    private final Map<String, PaymentStrategy> strategies;
    
    public CheckoutService(PaymentProcessor processor, 
                          CreditCardStrategy creditCard,
                          PayPalStrategy paypal,
                          CryptoStrategy crypto) {
        this.processor = processor;
        this.strategies = Map.of(
            "credit_card", creditCard,
            "paypal", paypal,
            "crypto", crypto
        );
    }
    
    public PaymentResult checkout(Order order, String paymentMethod) {
        PaymentStrategy strategy = strategies.get(paymentMethod);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown payment method: " + paymentMethod);
        }
        
        processor.setPaymentStrategy(strategy);
        
        PaymentDetails details = order.getPaymentDetails();
        return processor.processPayment(details);
    }
}

// Usage
CheckoutService checkout = new CheckoutService(processor, creditCard, paypal, crypto);

// User chose credit card
PaymentResult result = checkout.checkout(order, "credit_card");

// Same order, different method
PaymentResult result2 = checkout.checkout(order, "paypal");

How It Works

Here’s the flow when processing a payment:

sequenceDiagram
    participant Client
    participant CheckoutService
    participant PaymentProcessor
    participant Strategy
    participant ExternalService
    
    Client->>CheckoutService: checkout(order, "paypal")
    CheckoutService->>CheckoutService: Get PayPal strategy
    CheckoutService->>PaymentProcessor: setPaymentStrategy(paypal)
    CheckoutService->>PaymentProcessor: processPayment(details)
    PaymentProcessor->>Strategy: validate(details)
    Strategy-->>PaymentProcessor: true
    PaymentProcessor->>Strategy: process(details)
    Strategy->>ExternalService: API call
    ExternalService-->>Strategy: response
    Strategy-->>PaymentProcessor: PaymentResult
    PaymentProcessor-->>CheckoutService: PaymentResult
    CheckoutService-->>Client: PaymentResult

The PaymentProcessor doesn’t know which payment method it’s handling. It just calls the strategy interface methods.

Strategy with Lambda (Java 8+)

For simple strategies, you can use lambdas:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@FunctionalInterface
public interface SortStrategy<T> {
    void sort(List<T> items);
}

public class Sorter<T> {
    private SortStrategy<T> strategy;
    
    public void setStrategy(SortStrategy<T> strategy) {
        this.strategy = strategy;
    }
    
    public void sort(List<T> items) {
        strategy.sort(items);
    }
}

// Usage with lambdas
Sorter<Integer> sorter = new Sorter<>();

// Quick sort strategy
sorter.setStrategy(items -> {
    Collections.sort(items);
});

// Reverse sort strategy
sorter.setStrategy(items -> {
    Collections.sort(items, Collections.reverseOrder());
});

// Custom comparator strategy
sorter.setStrategy(items -> {
    items.sort((a, b) -> Integer.compare(Math.abs(a), Math.abs(b)));
});

Common Mistakes

1. Creating Strategy Objects Every Time

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Wasteful - creates new object for each payment
public PaymentResult process(String method, PaymentDetails details) {
    PaymentStrategy strategy;
    if (method.equals("credit_card")) {
        strategy = new CreditCardStrategy(fraudService, gateway);  // New instance
    }
    // ...
}

// Better - reuse strategy instances
private final Map<String, PaymentStrategy> strategies;

public PaymentResult process(String method, PaymentDetails details) {
    PaymentStrategy strategy = strategies.get(method);
    // ...
}

Strategies are typically stateless. Create them once and reuse.

2. Strategy with Too Much Context Knowledge

Strategies should be independent. If a strategy needs the entire context object:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Tight coupling
public class CreditCardStrategy implements PaymentStrategy {
    @Override
    public void process(PaymentProcessor processor) {
        processor.doThis();
        processor.doThat();  // Strategy knows too much about context
    }
}

// Better - pass only what's needed
public class CreditCardStrategy implements PaymentStrategy {
    @Override
    public PaymentResult process(PaymentDetails details) {
        // Works only with payment details
    }
}

3. Confusing Strategy with State

Strategy switches algorithms externally. State changes behavior based on internal conditions. If your “strategies” need to transition to each other automatically, you might want State pattern instead.

1
2
3
4
5
6
7
8
// This looks like Strategy but acts like State
if (currentState == PENDING) {
    setStrategy(new PendingStrategy());
} else if (currentState == APPROVED) {
    setStrategy(new ApprovedStrategy());
}

// If state transitions are driven by internal logic, use State pattern instead

Real-World Examples

Java Comparator: Collections.sort(list, comparator) - the comparator is a sorting strategy.

Spring Security: AuthenticationProvider implementations are strategies for different authentication methods.

Compression Libraries: GZIPOutputStream, ZipOutputStream - different compression strategies.

Validation Frameworks: Bean Validation allows custom validators as strategies.

State Pattern looks similar but changes behavior based on internal state. If the object decides when to switch, use State. If the client decides, use Strategy.

Template Method uses inheritance to vary parts of an algorithm. Strategy uses composition. Template Method is simpler but less flexible.

Decorator can wrap strategies to add behavior. For example, a logging decorator around any payment strategy.

Factory Method often creates strategy objects. The factory encapsulates the logic of which strategy to use.

Wrapping Up

Strategy encapsulates algorithms behind a common interface. The client picks which algorithm to use; the algorithm does its work without knowing about alternatives.

It’s composition over inheritance. Instead of subclassing to vary behavior, you compose objects with interchangeable parts.

Use Strategy when you have a family of related algorithms and need to switch between them. The pattern keeps each algorithm isolated, testable, and open for extension.


Further Reading: