Strategy Pattern explained simply
Ever written a method that is basically a giant if-else chain deciding which algorithm or behavior to use? You add one more option, and the whole thing gets longer and uglier. The Strategy Pattern exists to kill that mess for good.
What is the Strategy Pattern?
- Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
- Lets the algorithm vary independently from the clients that use it.
- Replaces conditional logic with composition – you inject the behavior you want instead of hard-coding it.
In short:
Same goal, different approaches. Pick one at runtime.
Real-Life Analogy
Open Google Maps and search for a destination. You will see options: driving, walking, cycling, public transit. The destination stays the same, but the strategy to get there changes completely. Each mode calculates a different route, a different ETA, and different directions.
You didn’t rewrite the entire navigation system for each mode. You just swapped the routing strategy. That is exactly what the Strategy Pattern does in code.
Structure
- Context (CheckoutService): Holds a reference to a strategy object. It delegates the actual work to the strategy instead of implementing it directly.
- Strategy (PaymentStrategy): An interface that all concrete strategies implement. Defines the contract – in our case, a single
pay()method. - Concrete Strategies (CreditCardPayment, PayPalPayment, CryptoPayment): Each one encapsulates a specific algorithm or behavior. They are interchangeable because they share the same interface.
The key insight here is that the CheckoutService never needs to know how a payment is processed. It just calls pay() on whatever strategy it has been given. New payment method? Write a new class, plug it in. Done.
Example in Java – A Payment Processing System
This is a classic use case. Your checkout flow needs to support multiple payment methods, and you want to add new ones without touching existing code.
PaymentStrategy Interface
public interface PaymentStrategy {
boolean pay(double amount);
}
One method. That is all the context needs to know about.
CreditCardPayment
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
private final String cardHolder;
public CreditCardPayment(String cardNumber, String cardHolder) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
}
@Override
public boolean pay(double amount) {
if (cardNumber == null || cardNumber.length() < 13) {
System.out.println("[CREDIT CARD] Invalid card number.");
return false;
}
// simulate charge processing
String masked = "****-" + cardNumber.substring(cardNumber.length() - 4);
System.out.println("[CREDIT CARD] Charged $" + amount + " to card " + masked + " (" + cardHolder + ")");
return true;
}
}
Validates the card, masks the number, processes the charge. Everything specific to credit card processing lives here and nowhere else.
PayPalPayment
public class PayPalPayment implements PaymentStrategy {
private final String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean pay(double amount) {
System.out.println("[PAYPAL] Authenticating account: " + email);
// simulate PayPal API authentication
if (email == null || !email.contains("@")) {
System.out.println("[PAYPAL] Authentication failed.");
return false;
}
System.out.println("[PAYPAL] Transferred $" + amount + " from " + email);
return true;
}
}
Different flow entirely – authenticate first, then transfer. But from the outside, the interface is identical.
CryptoPayment
public class CryptoPayment implements PaymentStrategy {
private final String walletAddress;
public CryptoPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public boolean pay(double amount) {
if (walletAddress == null || walletAddress.length() < 26) {
System.out.println("[CRYPTO] Invalid wallet address.");
return false;
}
String shortAddr = walletAddress.substring(0, 6) + "..." + walletAddress.substring(walletAddress.length() - 4);
System.out.println("[CRYPTO] Sending $" + amount + " in crypto to wallet " + shortAddr);
return true;
}
}
Validates the wallet address, truncates it for display, sends the transaction. Completely different internals, same interface.
CheckoutService (The Context)
public class CheckoutService {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public boolean checkout(double amount) {
if (paymentStrategy == null) {
System.out.println("No payment method selected.");
return false;
}
System.out.println("Processing checkout for $" + amount + "...");
boolean success = paymentStrategy.pay(amount);
if (success) {
System.out.println("Payment successful. Order confirmed.\n");
} else {
System.out.println("Payment failed. Please try again.\n");
}
return success;
}
}
Notice what is not here. No if-else chain. No switch statement. No mention of credit cards, PayPal, or crypto. The CheckoutService just delegates to whatever strategy is set. That is the entire point.
Putting It All Together
import java.util.Map;
public class MainProgram {
public static void main(String[] args) {
CheckoutService checkout = new CheckoutService();
// simulate user selecting payment methods dynamically
Map<String, PaymentStrategy> strategies = Map.of(
"credit_card", new CreditCardPayment("4539148803436467", "Alice Johnson"),
"paypal", new PayPalPayment("[email protected]"),
"crypto", new CryptoPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
);
// user picks credit card
String userChoice = "credit_card";
checkout.setPaymentStrategy(strategies.get(userChoice));
checkout.checkout(49.99);
// same user switches to PayPal for next purchase
userChoice = "paypal";
checkout.setPaymentStrategy(strategies.get(userChoice));
checkout.checkout(129.00);
// and crypto for another
userChoice = "crypto";
checkout.setPaymentStrategy(strategies.get(userChoice));
checkout.checkout(0.05);
}
}
Output:
Processing checkout for $49.99...
[CREDIT CARD] Charged $49.99 to card ****-6467 (Alice Johnson)
Payment successful. Order confirmed.
Processing checkout for $129.0...
[PAYPAL] Authenticating account: [email protected]
[PAYPAL] Transferred $129.0 from [email protected]
Payment successful. Order confirmed.
Processing checkout for $0.05...
[CRYPTO] Sending $0.05 in crypto to wallet 1A1zP1...fNa
Payment successful. Order confirmed.
The user switches payment methods at runtime. The CheckoutService does not change at all. No conditionals, no casting, no type checking. Just polymorphism doing its job.
Key Components
| Component | Role | In Our Example |
|---|---|---|
| Strategy | Interface defining the algorithm contract | PaymentStrategy |
| Concrete Strategy | A specific implementation of the algorithm | CreditCardPayment, PayPalPayment, CryptoPayment |
| Context | The class that uses a strategy | CheckoutService |
When to Use the Strategy Pattern
- You have multiple algorithms for the same task and want to switch between them at runtime.
- A class has a massive conditional block that selects different behavior based on type.
- You want to isolate algorithm-specific code from the business logic that uses it.
- You need to add new behaviors without modifying existing classes (Open/Closed Principle).
Advantages
- Eliminates conditionals: No more growing if-else or switch blocks.
- Open/Closed Principle: Add new strategies without touching existing code.
- Runtime flexibility: Swap algorithms on the fly based on configuration, user input, or system state.
- Testability: Each strategy can be unit tested in isolation.
- Single Responsibility: Each strategy class has one job.
Disadvantages
- More classes: Every new algorithm means a new class. For very simple cases, this can feel like overkill.
- Client awareness: The calling code needs to know which strategies exist and when to use each one. The decision logic moves somewhere – it doesn’t disappear entirely.
- Communication overhead: If strategies need shared state or data from the context, passing that data around can get awkward.
Real-World Use Cases
- Java Comparator:
Collections.sort(list, comparator)– the comparator is the strategy. Swap it to sort by name, date, price, whatever you want. - Spring Security AuthenticationProvider: Each provider (LDAP, database, OAuth) is a strategy for authenticating users. Spring tries them in order until one succeeds.
- Compression algorithms: A file archiver might support ZIP, GZIP, and BZIP2. Each is a compression strategy. The archiver just calls
compress()on whichever one the user picked. - Sorting strategies: A data pipeline might use quicksort for large datasets and insertion sort for small ones. Same interface, different strategies selected based on input size.
- Logging frameworks: Log to console, file, or remote server. Same log call, different output strategies.
Strategy vs Template Method Pattern
Both patterns deal with varying algorithms, so they get confused often. Here is the difference.
The Template Method Pattern defines the skeleton of an algorithm in a base class and lets subclasses override specific steps. The overall flow is fixed – only the details change. It uses inheritance.
The Strategy Pattern encapsulates entire algorithms as separate objects and lets you swap them at runtime. It uses composition.
In practical terms: if the algorithm structure is the same but certain steps vary, use Template Method. If the entire algorithm changes, use Strategy.
Think of it this way – Template Method says “follow these steps, but customize step 3.” Strategy says “here are three completely different ways to do this. Pick one.”
Wrapping Up
The Strategy Pattern is one of the most practical design patterns you will use. Anytime you catch yourself writing a conditional chain to pick between different algorithms or behaviors, stop and ask: can I extract these into strategy objects instead?
The payment processing example shows the real power – adding a new payment method like Apple Pay means writing one new class that implements PaymentStrategy. You don’t touch CheckoutService. You don’t touch any existing payment class. You just plug it in and it works.
That is clean, extensible design. No if-else required.