One-line Definition
Extracts interchangeable algorithms into separate classes, allowing the client to swap behaviors dynamically at runtime.
Problem
Suppose you are building an e-commerce checkout system. You need to calculate discounts. Initially, you write logic for a SeasonalDiscount. Then marketing demands a ClearanceDiscount, a VIPDiscount, and a FlatRateDiscount. If you shove all this logic into the Checkout class using a massive switch statement, the class becomes bloated, fragile, and impossible to test independently. The Strategy Pattern solves this by moving each discount algorithm into its own class, letting the Checkout class swap them out cleanly at runtime.
Real-world Analogy
Think of navigating to the airport using Google Maps. The map is the Context. You can choose the Driving Strategy, the Walking Strategy, or the Public Transit Strategy. The map's core job is to display a route, but it delegates the actual pathfinding math to whichever strategy you select. You can swap strategies mid-journey without breaking the app.
Structure
- Client
- Creates a specific Strategy object and passes it to the Context.
- Context
- Maintains a reference to a Strategy object and delegates execution to it.
- Strategy
- Interface common to all supported algorithms.
- ConcreteStrategy
- Implements a specific algorithm following the Strategy interface.
Diagram
classDiagram
class Client {
}
class Context {
-Strategy strategy
+setStrategy(Strategy)
+executeStrategy()
}
class Strategy {
<<interface>>
+execute(data)
}
class ConcreteStrategyA {
+execute(data)
}
class ConcreteStrategyB {
+execute(data)
}
Client --> Context
Client --> ConcreteStrategyA
Context o-- Strategy : delegates
ConcreteStrategyA ..|> Strategy
ConcreteStrategyB ..|> Strategy
Code Walkthrough
Notice how the ShoppingCart (Context) has zero hardcoded logic for payment processing. It simply delegates the pay() action to whatever PaymentStrategy was injected by the client.
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardStrategy implements PaymentStrategy {
private final String cardNumber;
public CreditCardStrategy(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid ₹" + amount + " using Credit Card ending in " + cardNumber.substring(12));
}
}
class UPIStrategy implements PaymentStrategy {
private final String upiId;
public UPIStrategy(String upiId) {
this.upiId = upiId;
}
@Override
public void pay(int amount) {
System.out.println("Paid ₹" + amount + " using UPI ID: " + upiId);
}
}
class ShoppingCart {
private PaymentStrategy paymentStrategy;
private int amount = 0;
public void addItem(int price) {
amount += price;
}
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout() {
if (paymentStrategy == null) {
System.out.println("Please select a payment method.");
return;
}
paymentStrategy.pay(amount);
}
}
class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.addItem(1500);
cart.addItem(300);
// Client chooses the UPI strategy dynamically
cart.setPaymentStrategy(new UPIStrategy("user@ybl"));
cart.checkout();
// Swap strategy at runtime without altering the cart
cart.setPaymentStrategy(new CreditCardStrategy("1111222233334444"));
cart.checkout();
}
}
Bad vs Good
Bad Approach
Problems
- The
ShoppingCartclass is bloated with the business logic for every single payment gateway. - Adding a new payment method requires modifying the
ShoppingCart, breaking the Open/Closed Principle.
class ShoppingCart {
public void checkout(String method, int amount) {
if (method.equals("CREDIT_CARD")) {
// 50 lines of credit card logic
} else if (method.equals("UPI")) {
// 50 lines of UPI logic
} else if (method.equals("PAYPAL")) {
// 50 lines of PayPal logic
}
}
}
Better Approach
Improvements
- The
ShoppingCartis entirely decoupled from payment logic. - New payment methods are added simply by implementing an interface.
- Favoring composition (Strategy) over inheritance prevents rigid, unchangeable hierarchies.
// Pure delegation. ShoppingCart delegates heavy lifting to the Strategy.
public void checkout() {
paymentStrategy.pay(amount);
}
Pros vs Cons
| Pros | Cons |
|---|---|
Completely eliminates massive conditional statements (switch/if) | Clients must understand the differences between strategies to pick the right one |
| Allows swapping algorithms dynamically at runtime | Can dramatically increase the number of classes in a project |
| Strictly enforces the Open/Closed Principle | In modern languages, simple strategies can be replaced with Lambdas/Functions |
| Isolates complex, volatile algorithm logic from the main Context | |
| Favors Composition over Inheritance |
When to Use
- You have multiple variations of an algorithm (e.g., sorting, routing, pricing, compression) and want to swap them at runtime.
- You want to isolate the business logic of an algorithm from the class that uses it.
- Your class contains a massive conditional operator that switches between different behaviors.
When Not to Use
- You only have a couple of algorithms and they rarely change.
- The differences between the algorithms are trivial (e.g., just a single variable changing). Modern Java can handle this effortlessly with
java.util.function.Consumeror Lambdas.
Real-world Examples
java.util.Collections#sort(List, Comparator)(TheComparatoris a Strategy!).- Layout Managers in Java Swing (
BorderLayout,FlowLayout). - Spring's
Resourceinstantiation strategies or Spring Security'sPasswordEncoder.
Key Takeaway
The Strategy Pattern replaces hard-coded conditional logic with interchangeable behavior objects. By delegating an operation to a Strategy interface, a Context class can effortlessly swap its behavior at runtime. Use it when dealing with families of algorithms (sorting, payment, validation), but leverage Lambdas in modern languages to avoid class explosion for simple strategies.