Strategy Design Pattern
1. What is Strategy Pattern?
The Strategy pattern is a GOF Behavioral Pattern. Strategy means planning. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets the algorithm vary independently from the clients that use it.
One line: Keep two collaborating classes as loosely coupled interchangeable parts. Instead of hardcoding a dependency, inject it through an interface.
The class that uses other class is called target class (or context). The class that is acting as helper to the main class (target class) is called dependent class (or strategy).
| Target Class | Relationship | Dependent Class |
|---|---|---|
| Flipkart | uses/needs | DTDC, BlueDart, DHL (Courier Services) |
| Vehicle | uses/needs | Engine |
| Cricketor | uses/needs | CricketBat |
Strategy DP says we can describe the Java classes of dependency management (classes in collaboration) as loosely coupled interchangeable parts. We can replace any dependent class with another dependent class without modifying the target class.
2. Three Rules of Strategy Pattern
- Favour composition over inheritance
- Code to interfaces/abstract classes, never code to implementation classes (concrete classes)
- Code must be open for extension and closed for modification
3. Rule a) Favour Composition Over Inheritance
Inheritance is IS-A relationship. Composition is HAS-A relationship.
// Inheritance (IS-A): class B extends A
class A { }
class B extends A { } // B is-a A (IS-A relationship)
// Composition (HAS-A): class B has reference to A
class A { }
class B {
private A a; // B has-a A (HAS-A relationship)
}
If a class wants to use everything from another class (both properties and methods), use inheritance (IS-A). If a class wants to use specific properties and methods of another class, use composition (HAS-A).
Three limitations of inheritance
a) No multiple inheritance in Java
Java does not support multiple inheritance using classes. Interfaces in Java are not given in the aspect of inheritance; they are given for achieving polymorphism with loose coupling.
b) Inheritance code becomes fragile (easily breakable)
// Problem: with inheritance
class A {
public int m1() { return 100; }
}
class B extends A {
public int m1() { // overrides A.m1()
return 200;
}
}
class C extends A { } // inherits m1()
class D extends B { } // inherits overridden m1()
// Note: if we modify the signature of m1() in class A:
// - method m1() in B that was only overriding immediately
// becomes a new different method (no longer overriding)
// - all extending hierarchy in A's entire hierarchy of B,
// classes will distributed indicating "code is fragile"
class A {
public void m1() { return 100; }
}
class B {
private A a = new A();
public void m1() {
int result = a.m1(); // delegates to A
return result;
}
}
// If we modify the signature of m1() in class A, only in B,
// the m1() method's code will be distributed. But other classes
// extending B will not be distributed.
// Using composition we have minimized the problem.
c) Inheritance makes Unit Testing very complex
4. Rule b) Code to Interfaces, Not Concrete Classes
// Target class directly uses concrete dependent classes
public class Communication {
private DTDC dtdc; // hardcoded to DTDC
private DHL dhl; // hardcoded to DHL
private BlueDart bluedart; // hardcoded to BlueDart
public String shipping(String[] items, double[] prices) {
// must decide which courier to use with if-else
// tightly coupled to all concrete classes
}
}
If the target and dependent classes are among the same package and directly referencing each other, they are called tightly coupled components.
// Target class holds interface reference (loosely coupled)
public class Communication {
private Courier courier; // interface reference
public void setCourier(Courier courier) {
this.courier = courier; // inject at runtime
}
public String shipping(String[] items, double[] prices) {
courier.deliver(1001); // uses interface method
}
}
If the objects of dependency live among interfaces with no direct reference, they are called loosely coupled components. In the target class, we able to change the dependent class (of target class) without touching the source code of target class. So we can say the target and dependent classes are loosely coupled classes.
Modes of dependent class injection
- Constructor-based injection: Dependent classes implementing common interface/base class can be injected in constructor. At level class or child class, along with setter method, parameterized constructor is for support.
- Setter-based injection: Inject via setter method at runtime.
- Interface-based injection: Inject via interface method.
// Constructor-based injection
public class Communication {
private Courier courier;
public Communication(Courier courier) { // injected via constructor
this.courier = courier;
}
}
// Setter-based injection
public class Communication {
private Courier courier;
public void setCourier(Courier courier) { // injected via setter
this.courier = courier;
}
}
5. Rule c) Open for Extension, Closed for Modification
- Open for extension: If an implementation (strategy) is not property, we can swap out with another strategy by extending classes (the contract/interface). We can add new strategies without modifying existing code. This is also how any code is added to the target class outside the target class.
- Closed for modification: When dependent classes are called/created/injected by implementing the common interface/class, our code is open for extension. We can modify our code in limited ways: (a) By keeping the classes as final classes (so extending will not be a problem), (b) If the class is not final and if there is no need to modify the logic in its given class methods, (c) By defining methods of upper class as final methods. Since methods of upper class are declared or defined in the sub class, we can say the code of the parent class is closed for modification.
6. Implementation: Courier Delivery System
Strategy DP Components
| Component | Role | In Our Example |
|---|---|---|
| Context (Target) | The class that uses a strategy. Holds interface reference. | Communication |
| Strategy Interface | Common interface for all algorithms. | Courier |
| Concrete Strategies | Multiple implementations of the interface. | DHL, BlueDart, DTDC |
| Client | Creates strategy and injects it into context. | ClientApp |
Courier.java (Strategy interface)
package com.nt.strategy;
public interface Courier {
void deliver(int orderId);
String getDeliveryEstimate(String destination);
}
Concrete strategies
// DHL.java
package com.nt.strategy;
public class DHL implements Courier {
public DHL() {
System.out.println("DHL:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("DHL delivering order #" + orderId + " via express air");
}
@Override
public String getDeliveryEstimate(String destination) {
return "2-3 business days to " + destination;
}
}
// BlueDart.java
package com.nt.strategy;
public class BlueDart implements Courier {
public BlueDart() {
System.out.println("BlueDart:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("BlueDart delivering order #" + orderId + " via ground");
}
@Override
public String getDeliveryEstimate(String destination) {
return "4-5 business days to " + destination;
}
}
// DTDC.java
package com.nt.strategy;
public class DTDC implements Courier {
public DTDC() {
System.out.println("DTDC:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("DTDC delivering order #" + orderId + " via economy");
}
@Override
public String getDeliveryEstimate(String destination) {
return "5-7 business days to " + destination;
}
}
CourierFactory.java
package com.nt.factory;
import com.nt.strategy.*;
public class CourierFactory {
public static Courier getInstance(String type) {
if (type == null) {
throw new IllegalArgumentException("Courier type cannot be null");
}
switch (type.toLowerCase()) {
case "dhl": return new DHL();
case "bluedart": return new BlueDart();
case "dtdc": return new DTDC();
default: throw new IllegalArgumentException("Unknown courier: " + type);
}
}
}
Communication.java (Context / Target class)
package com.nt.target;
import com.nt.strategy.Courier;
public class Communication {
private Courier courier; // HAS-A (composition, not inheritance)
// Setter-based injection
public void setCourier(Courier courier) {
this.courier = courier;
}
public void shipping(String[] items, double[] prices) {
System.out.println("Processing shipment of " + items.length + " items...");
double total = 0;
for (int i = 0; i < items.length; i++) {
System.out.println(" Item: " + items[i] + " | Price: Rs " + prices[i]);
total += prices[i];
}
System.out.println(" Total: Rs " + total);
// Delegate delivery to the injected strategy
courier.deliver(1001);
System.out.println(" Estimate: " + courier.getDeliveryEstimate("Hyderabad"));
}
}
Client test
package com.nt.test;
import com.nt.target.Communication;
import com.nt.strategy.Courier;
import com.nt.factory.CourierFactory;
public class ClientApp {
public static void main(String[] args) {
Communication comm = new Communication();
String[] items = {"Java Book", "Laptop Stand"};
double[] prices = {500.0, 1200.0};
// Strategy 1: DHL
System.out.println("=== Using DHL ===");
Courier dhl = CourierFactory.getInstance("dhl");
comm.setCourier(dhl);
comm.shipping(items, prices);
System.out.println();
// Strategy 2: Switch to BlueDart at runtime (no code change in Communication)
System.out.println("=== Switching to BlueDart ===");
Courier bluedart = CourierFactory.getInstance("bluedart");
comm.setCourier(bluedart);
comm.shipping(items, prices);
}
}
The Communication class was NOT modified when switching from DHL to BlueDart. We only changed the injected strategy object via setCourier(). The target and dependent classes are loosely coupled.
7. Strategy Pattern and Spring Dependency Injection
Spring Dependency Injection is fundamentally the Strategy pattern. Spring creates strategy objects (beans) and injects them into target classes. The XML or annotation configuration determines which concrete strategy is used. Changing the strategy requires only a configuration change, not a code change.
// Spring XML configuration (beans.xml)
<bean id="dhlCourier" class="com.nt.strategy.DHL" />
<bean id="bluedartCourier" class="com.nt.strategy.BlueDart" />
<bean id="communication" class="com.nt.target.Communication">
<property name="courier" ref="dhlCourier" />
<!-- Switch by changing ref to "bluedartCourier" -->
</bean>
// Spring Boot annotation equivalent:
@Service
public class Communication {
@Autowired
@Qualifier("dhl")
private Courier courier; // Spring injects the strategy
}
In Spring, the BeanFactory.getBean() call creates and returns the strategy object. The ApplicationContext manages the entire lifecycle. This is the Strategy pattern automated by the framework.
8. Full-Length Industry Project: Shopping Cart with Payment Strategy
National Bank of India's e-commerce partner needs a shopping cart that supports multiple payment methods: UPI, Credit Card, and Net Banking. The payment method is selected by the customer at checkout. The cart must be able to switch payment strategies at runtime without modifying the cart logic. New payment methods (Wallet, EMI) must be addable without changing existing code.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
CartItem.java
package com.nbi.shop.model;
import java.math.BigDecimal;
public class CartItem {
private final String name;
private final BigDecimal price;
private final int quantity;
public CartItem(String name, BigDecimal price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
public int getQuantity() { return quantity; }
public BigDecimal getSubtotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
@Override
public String toString() {
return name + " x" + quantity + " @ Rs " + price + " = Rs " + getSubtotal();
}
}
PaymentResult.java
package com.nbi.shop.model;
import java.util.UUID;
public class PaymentResult {
private final boolean success;
private final String transactionId;
private final String message;
private PaymentResult(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
public static PaymentResult success(String message) {
String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
return new PaymentResult(true, txnId, message);
}
public static PaymentResult failure(String message) {
return new PaymentResult(false, null, message);
}
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
public String getMessage() { return message; }
@Override
public String toString() {
return success
? "SUCCESS | " + transactionId + " | " + message
: "FAILED | " + message;
}
}
PaymentStrategy.java (Strategy interface)
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public interface PaymentStrategy {
PaymentResult pay(BigDecimal amount);
String getName();
boolean validate();
}
UPIStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class UPIStrategy implements PaymentStrategy {
private final String upiId;
private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000");
public UPIStrategy(String upiId) {
this.upiId = upiId;
}
@Override
public boolean validate() {
return upiId != null && upiId.contains("@");
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Invalid UPI ID: " + upiId);
}
if (amount.compareTo(MAX_AMOUNT) > 0) {
return PaymentResult.failure("UPI limit exceeded. Max: Rs " + MAX_AMOUNT);
}
System.out.println(" [UPI] Sending request to " + upiId);
System.out.println(" [UPI] Waiting for approval...");
System.out.println(" [UPI] Payment approved");
return PaymentResult.success("Paid Rs " + amount + " via UPI (" + upiId + ")");
}
@Override
public String getName() { return "UPI"; }
}
CreditCardStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class CreditCardStrategy implements PaymentStrategy {
private final String cardNumber;
private final String cvv;
private final String expiry;
public CreditCardStrategy(String cardNumber, String cvv, String expiry) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiry = expiry;
}
@Override
public boolean validate() {
return cardNumber != null && cardNumber.length() == 16
&& cvv != null && cvv.length() == 3
&& expiry != null && !expiry.isEmpty();
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Invalid card details");
}
String masked = "****-" + cardNumber.substring(12);
System.out.println(" [CARD] Authorizing card " + masked);
System.out.println(" [CARD] Charging Rs " + amount);
System.out.println(" [CARD] Payment settled");
return PaymentResult.success("Paid Rs " + amount + " via Credit Card (" + masked + ")");
}
@Override
public String getName() { return "Credit Card"; }
}
NetBankingStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class NetBankingStrategy implements PaymentStrategy {
private final String bankCode;
public NetBankingStrategy(String bankCode) {
this.bankCode = bankCode;
}
@Override
public boolean validate() {
return bankCode != null && !bankCode.isEmpty();
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Bank code is required");
}
System.out.println(" [NETBANK] Redirecting to " + bankCode + " portal");
System.out.println(" [NETBANK] User approved transaction");
System.out.println(" [NETBANK] Debit confirmed");
return PaymentResult.success("Paid Rs " + amount + " via Net Banking (" + bankCode + ")");
}
@Override
public String getName() { return "Net Banking"; }
}
ShoppingCart.java (Context / Target class)
package com.nbi.shop.cart;
import com.nbi.shop.model.CartItem;
import com.nbi.shop.model.PaymentResult;
import com.nbi.shop.strategy.PaymentStrategy;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
private PaymentStrategy paymentStrategy; // HAS-A (composition)
public void addItem(CartItem item) {
items.add(item);
}
public void removeItem(String name) {
items.removeIf(item -> item.getName().equals(name));
}
public BigDecimal getTotal() {
return items.stream()
.map(CartItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Strategy injection via setter
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void displayCart() {
System.out.println("Cart Items:");
for (CartItem item : items) {
System.out.println(" " + item);
}
System.out.println(" Total: Rs " + getTotal());
}
public PaymentResult checkout() {
if (items.isEmpty()) {
return PaymentResult.failure("Cart is empty");
}
if (paymentStrategy == null) {
return PaymentResult.failure("No payment method selected");
}
BigDecimal total = getTotal();
System.out.println("Checking out with " + paymentStrategy.getName()
+ " | Amount: Rs " + total);
PaymentResult result = paymentStrategy.pay(total);
if (result.isSuccess()) {
items.clear();
}
return result;
}
}
Application.java
package com.nbi.shop;
import com.nbi.shop.cart.ShoppingCart;
import com.nbi.shop.model.CartItem;
import com.nbi.shop.model.PaymentResult;
import com.nbi.shop.strategy.*;
import java.math.BigDecimal;
public class Application {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Add items
cart.addItem(new CartItem("Java: The Complete Reference", new BigDecimal("799"), 1));
cart.addItem(new CartItem("Mechanical Keyboard", new BigDecimal("3500"), 1));
cart.addItem(new CartItem("USB-C Hub", new BigDecimal("1200"), 2));
// Scenario 1: Pay with UPI
System.out.println("=== Scenario 1: UPI Payment ===");
cart.displayCart();
cart.setPaymentStrategy(new UPIStrategy("rajesh@icici"));
PaymentResult r1 = cart.checkout();
System.out.println("Result: " + r1);
System.out.println();
// Scenario 2: New cart, pay with Credit Card
System.out.println("=== Scenario 2: Credit Card Payment ===");
cart.addItem(new CartItem("Monitor", new BigDecimal("15000"), 1));
cart.addItem(new CartItem("Mouse", new BigDecimal("800"), 1));
cart.displayCart();
cart.setPaymentStrategy(new CreditCardStrategy("4111222233334444", "123", "12/28"));
PaymentResult r2 = cart.checkout();
System.out.println("Result: " + r2);
System.out.println();
// Scenario 3: Switch strategy at runtime to Net Banking
System.out.println("=== Scenario 3: Net Banking (runtime switch) ===");
cart.addItem(new CartItem("Standing Desk", new BigDecimal("25000"), 1));
cart.displayCart();
cart.setPaymentStrategy(new NetBankingStrategy("SBI"));
PaymentResult r3 = cart.checkout();
System.out.println("Result: " + r3);
System.out.println();
// Scenario 4: Validation failure (invalid UPI)
System.out.println("=== Scenario 4: Invalid UPI ===");
cart.addItem(new CartItem("Book", new BigDecimal("500"), 1));
cart.setPaymentStrategy(new UPIStrategy("invalid_no_at"));
PaymentResult r4 = cart.checkout();
System.out.println("Result: " + r4);
System.out.println();
// Scenario 5: Empty cart
System.out.println("=== Scenario 5: Empty Cart ===");
ShoppingCart emptyCart = new ShoppingCart();
emptyCart.setPaymentStrategy(new UPIStrategy("test@bank"));
PaymentResult r5 = emptyCart.checkout();
System.out.println("Result: " + r5);
System.out.println();
// Scenario 6: No payment strategy set
System.out.println("=== Scenario 6: No Payment Method ===");
ShoppingCart noPayCart = new ShoppingCart();
noPayCart.addItem(new CartItem("Item", new BigDecimal("100"), 1));
PaymentResult r6 = noPayCart.checkout();
System.out.println("Result: " + r6);
}
}
Expected Output
Key observations from the project
- ShoppingCart never imports concrete strategy classes. It depends only on the
PaymentStrategyinterface. Composition, not inheritance. - Strategy is swapped at runtime via
setPaymentStrategy(). Scenario 1 uses UPI, scenario 2 switches to Credit Card, scenario 3 switches to Net Banking. The ShoppingCart code does not change. - Adding a Wallet strategy: Create
WalletStrategy implements PaymentStrategy, inject it viasetPaymentStrategy(new WalletStrategy()). Zero changes to ShoppingCart or other strategies. - Each strategy validates independently. UPI checks for @ in ID. Credit Card checks 16-digit number. Net Banking checks bank code. The cart does not know the validation rules.
- Six scenarios cover happy path, runtime switch, validation failure, empty cart, and missing strategy. All demonstrate loose coupling.
- Spring equivalent: Replace
setPaymentStrategy()with@Autowired @Qualifier("upi")and the framework handles injection.
9. Strategy vs Template Method
| Aspect | Strategy | Template Method |
|---|---|---|
| Mechanism | Composition (HAS-A interface reference) | Inheritance (abstract class + subclasses) |
| What varies | The entire algorithm | Some steps of the algorithm |
| Algorithm locked? | No. Each strategy defines its own flow | Yes. Template method is final |
| Runtime swap | Yes. Change strategy object at runtime | No. Object type fixed at creation |
| Open/Closed | Add new strategy class. No existing changes. | Add new subclass. No existing changes. |
| Testing | Easy. Mock the interface. | Harder. Must test inherited + own methods. |
| Use when | Multiple interchangeable algorithms | Fixed algorithm with varying steps |
10. When to Use and When NOT to Use
When to use Strategy
| Scenario | Example | Why Strategy |
|---|---|---|
| Multiple payment methods | UPI, Card, NetBanking, Wallet | Customer picks at runtime. Cart logic unchanged. |
| Sorting algorithms | BubbleSort, QuickSort, MergeSort | Choose based on data size. Swap without changing caller. |
| Compression | ZIP, GZIP, LZ4 | Choose based on file type. All implement same interface. |
| Authentication | JWT, OAuth2, Basic Auth | Different systems use different auth. Strategy lets you swap. |
| Notification channels | Email, SMS, Push, Slack | User preference determines channel. Inject at runtime. |
When NOT to use Strategy
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Only one algorithm | No alternatives to swap between | Direct implementation |
| Algorithm never changes at runtime | If decided at compile time, strategy adds unnecessary indirection | Template Method or simple if/else |
| Too many strategies | If strategies proliferate and each is trivial, the pattern adds class count for little benefit | Lambda expressions or functional approach |
11. Quick Reference Card
| Component | Role | In Shopping Cart Project |
|---|---|---|
| Context (Target) | Uses strategy via interface reference | ShoppingCart |
| Strategy Interface | Common contract for all algorithms | PaymentStrategy |
| Concrete Strategies | Each implements one algorithm | UPIStrategy, CreditCardStrategy, NetBankingStrategy |
| Client | Creates strategy, injects into context | Application |
| Injection method | Setter or constructor | setPaymentStrategy() |
Strategy Design Pattern in Java
1. What is Strategy Pattern?
The Strategy pattern is a GOF Behavioral Pattern. Strategy means planning. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern lets the algorithm vary independently from the clients that use it.
One line: Keep two collaborating classes as loosely coupled interchangeable parts. Instead of hardcoding a dependency, inject it through an interface.
The class that uses other class is called target class (or context). The class that is acting as helper to the main class (target class) is called dependent class (or strategy).
| Target Class | Relationship | Dependent Class |
|---|---|---|
| Flipkart | uses/needs | DTDC, BlueDart, DHL (Courier Services) |
| Vehicle | uses/needs | Engine |
| Cricketor | uses/needs | CricketBat |
Strategy DP says we can describe the Java classes of dependency management (classes in collaboration) as loosely coupled interchangeable parts. We can replace any dependent class with another dependent class without modifying the target class.
2. Three Rules of Strategy Pattern
- Favour composition over inheritance
- Code to interfaces/abstract classes, never code to implementation classes (concrete classes)
- Code must be open for extension and closed for modification
3. Rule a) Favour Composition Over Inheritance
Inheritance is IS-A relationship. Composition is HAS-A relationship.
// Inheritance (IS-A): class B extends A
class A { }
class B extends A { } // B is-a A (IS-A relationship)
// Composition (HAS-A): class B has reference to A
class A { }
class B {
private A a; // B has-a A (HAS-A relationship)
}
If a class wants to use everything from another class (both properties and methods), use inheritance (IS-A). If a class wants to use specific properties and methods of another class, use composition (HAS-A).
Three limitations of inheritance
a) No multiple inheritance in Java
Java does not support multiple inheritance using classes. Interfaces in Java are not given in the aspect of inheritance; they are given for achieving polymorphism with loose coupling.
b) Inheritance code becomes fragile (easily breakable)
// Problem: with inheritance
class A {
public int m1() { return 100; }
}
class B extends A {
public int m1() { // overrides A.m1()
return 200;
}
}
class C extends A { } // inherits m1()
class D extends B { } // inherits overridden m1()
// Note: if we modify the signature of m1() in class A:
// - method m1() in B that was only overriding immediately
// becomes a new different method (no longer overriding)
// - all extending hierarchy in A's entire hierarchy of B,
// classes will distributed indicating "code is fragile"
class A {
public void m1() { return 100; }
}
class B {
private A a = new A();
public void m1() {
int result = a.m1(); // delegates to A
return result;
}
}
// If we modify the signature of m1() in class A, only in B,
// the m1() method's code will be distributed. But other classes
// extending B will not be distributed.
// Using composition we have minimized the problem.
c) Inheritance makes Unit Testing very complex
4. Rule b) Code to Interfaces, Not Concrete Classes
// Target class directly uses concrete dependent classes
public class Communication {
private DTDC dtdc; // hardcoded to DTDC
private DHL dhl; // hardcoded to DHL
private BlueDart bluedart; // hardcoded to BlueDart
public String shipping(String[] items, double[] prices) {
// must decide which courier to use with if-else
// tightly coupled to all concrete classes
}
}
If the target and dependent classes are among the same package and directly referencing each other, they are called tightly coupled components.
// Target class holds interface reference (loosely coupled)
public class Communication {
private Courier courier; // interface reference
public void setCourier(Courier courier) {
this.courier = courier; // inject at runtime
}
public String shipping(String[] items, double[] prices) {
courier.deliver(1001); // uses interface method
}
}
If the objects of dependency live among interfaces with no direct reference, they are called loosely coupled components. In the target class, we able to change the dependent class (of target class) without touching the source code of target class. So we can say the target and dependent classes are loosely coupled classes.
Modes of dependent class injection
- Constructor-based injection: Dependent classes implementing common interface/base class can be injected in constructor. At level class or child class, along with setter method, parameterized constructor is for support.
- Setter-based injection: Inject via setter method at runtime.
- Interface-based injection: Inject via interface method.
// Constructor-based injection
public class Communication {
private Courier courier;
public Communication(Courier courier) { // injected via constructor
this.courier = courier;
}
}
// Setter-based injection
public class Communication {
private Courier courier;
public void setCourier(Courier courier) { // injected via setter
this.courier = courier;
}
}
5. Rule c) Open for Extension, Closed for Modification
- Open for extension: If an implementation (strategy) is not property, we can swap out with another strategy by extending classes (the contract/interface). We can add new strategies without modifying existing code. This is also how any code is added to the target class outside the target class.
- Closed for modification: When dependent classes are called/created/injected by implementing the common interface/class, our code is open for extension. We can modify our code in limited ways: (a) By keeping the classes as final classes (so extending will not be a problem), (b) If the class is not final and if there is no need to modify the logic in its given class methods, (c) By defining methods of upper class as final methods. Since methods of upper class are declared or defined in the sub class, we can say the code of the parent class is closed for modification.
6. Implementation: Courier Delivery System
Strategy DP Components
| Component | Role | In Our Example |
|---|---|---|
| Context (Target) | The class that uses a strategy. Holds interface reference. | Communication |
| Strategy Interface | Common interface for all algorithms. | Courier |
| Concrete Strategies | Multiple implementations of the interface. | DHL, BlueDart, DTDC |
| Client | Creates strategy and injects it into context. | ClientApp |
Courier.java (Strategy interface)
package com.nt.strategy;
public interface Courier {
void deliver(int orderId);
String getDeliveryEstimate(String destination);
}
Concrete strategies
// DHL.java
package com.nt.strategy;
public class DHL implements Courier {
public DHL() {
System.out.println("DHL:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("DHL delivering order #" + orderId + " via express air");
}
@Override
public String getDeliveryEstimate(String destination) {
return "2-3 business days to " + destination;
}
}
// BlueDart.java
package com.nt.strategy;
public class BlueDart implements Courier {
public BlueDart() {
System.out.println("BlueDart:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("BlueDart delivering order #" + orderId + " via ground");
}
@Override
public String getDeliveryEstimate(String destination) {
return "4-5 business days to " + destination;
}
}
// DTDC.java
package com.nt.strategy;
public class DTDC implements Courier {
public DTDC() {
System.out.println("DTDC:: 0-param constructor");
}
@Override
public void deliver(int orderId) {
System.out.println("DTDC delivering order #" + orderId + " via economy");
}
@Override
public String getDeliveryEstimate(String destination) {
return "5-7 business days to " + destination;
}
}
CourierFactory.java
package com.nt.factory;
import com.nt.strategy.*;
public class CourierFactory {
public static Courier getInstance(String type) {
if (type == null) {
throw new IllegalArgumentException("Courier type cannot be null");
}
switch (type.toLowerCase()) {
case "dhl": return new DHL();
case "bluedart": return new BlueDart();
case "dtdc": return new DTDC();
default: throw new IllegalArgumentException("Unknown courier: " + type);
}
}
}
Communication.java (Context / Target class)
package com.nt.target;
import com.nt.strategy.Courier;
public class Communication {
private Courier courier; // HAS-A (composition, not inheritance)
// Setter-based injection
public void setCourier(Courier courier) {
this.courier = courier;
}
public void shipping(String[] items, double[] prices) {
System.out.println("Processing shipment of " + items.length + " items...");
double total = 0;
for (int i = 0; i < items.length; i++) {
System.out.println(" Item: " + items[i] + " | Price: Rs " + prices[i]);
total += prices[i];
}
System.out.println(" Total: Rs " + total);
// Delegate delivery to the injected strategy
courier.deliver(1001);
System.out.println(" Estimate: " + courier.getDeliveryEstimate("Hyderabad"));
}
}
Client test
package com.nt.test;
import com.nt.target.Communication;
import com.nt.strategy.Courier;
import com.nt.factory.CourierFactory;
public class ClientApp {
public static void main(String[] args) {
Communication comm = new Communication();
String[] items = {"Java Book", "Laptop Stand"};
double[] prices = {500.0, 1200.0};
// Strategy 1: DHL
System.out.println("=== Using DHL ===");
Courier dhl = CourierFactory.getInstance("dhl");
comm.setCourier(dhl);
comm.shipping(items, prices);
System.out.println();
// Strategy 2: Switch to BlueDart at runtime (no code change in Communication)
System.out.println("=== Switching to BlueDart ===");
Courier bluedart = CourierFactory.getInstance("bluedart");
comm.setCourier(bluedart);
comm.shipping(items, prices);
}
}
The Communication class was NOT modified when switching from DHL to BlueDart. We only changed the injected strategy object via setCourier(). The target and dependent classes are loosely coupled.
7. Strategy Pattern and Spring Dependency Injection
Spring Dependency Injection is fundamentally the Strategy pattern. Spring creates strategy objects (beans) and injects them into target classes. The XML or annotation configuration determines which concrete strategy is used. Changing the strategy requires only a configuration change, not a code change.
// Spring XML configuration (beans.xml)
<bean id="dhlCourier" class="com.nt.strategy.DHL" />
<bean id="bluedartCourier" class="com.nt.strategy.BlueDart" />
<bean id="communication" class="com.nt.target.Communication">
<property name="courier" ref="dhlCourier" />
<!-- Switch by changing ref to "bluedartCourier" -->
</bean>
// Spring Boot annotation equivalent:
@Service
public class Communication {
@Autowired
@Qualifier("dhl")
private Courier courier; // Spring injects the strategy
}
In Spring, the BeanFactory.getBean() call creates and returns the strategy object. The ApplicationContext manages the entire lifecycle. This is the Strategy pattern automated by the framework.
8. Full-Length Industry Project: Shopping Cart with Payment Strategy
National Bank of India's e-commerce partner needs a shopping cart that supports multiple payment methods: UPI, Credit Card, and Net Banking. The payment method is selected by the customer at checkout. The cart must be able to switch payment strategies at runtime without modifying the cart logic. New payment methods (Wallet, EMI) must be addable without changing existing code.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
CartItem.java
package com.nbi.shop.model;
import java.math.BigDecimal;
public class CartItem {
private final String name;
private final BigDecimal price;
private final int quantity;
public CartItem(String name, BigDecimal price, int quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
public int getQuantity() { return quantity; }
public BigDecimal getSubtotal() {
return price.multiply(BigDecimal.valueOf(quantity));
}
@Override
public String toString() {
return name + " x" + quantity + " @ Rs " + price + " = Rs " + getSubtotal();
}
}
PaymentResult.java
package com.nbi.shop.model;
import java.util.UUID;
public class PaymentResult {
private final boolean success;
private final String transactionId;
private final String message;
private PaymentResult(boolean success, String transactionId, String message) {
this.success = success;
this.transactionId = transactionId;
this.message = message;
}
public static PaymentResult success(String message) {
String txnId = "TXN-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
return new PaymentResult(true, txnId, message);
}
public static PaymentResult failure(String message) {
return new PaymentResult(false, null, message);
}
public boolean isSuccess() { return success; }
public String getTransactionId() { return transactionId; }
public String getMessage() { return message; }
@Override
public String toString() {
return success
? "SUCCESS | " + transactionId + " | " + message
: "FAILED | " + message;
}
}
PaymentStrategy.java (Strategy interface)
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public interface PaymentStrategy {
PaymentResult pay(BigDecimal amount);
String getName();
boolean validate();
}
UPIStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class UPIStrategy implements PaymentStrategy {
private final String upiId;
private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000");
public UPIStrategy(String upiId) {
this.upiId = upiId;
}
@Override
public boolean validate() {
return upiId != null && upiId.contains("@");
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Invalid UPI ID: " + upiId);
}
if (amount.compareTo(MAX_AMOUNT) > 0) {
return PaymentResult.failure("UPI limit exceeded. Max: Rs " + MAX_AMOUNT);
}
System.out.println(" [UPI] Sending request to " + upiId);
System.out.println(" [UPI] Waiting for approval...");
System.out.println(" [UPI] Payment approved");
return PaymentResult.success("Paid Rs " + amount + " via UPI (" + upiId + ")");
}
@Override
public String getName() { return "UPI"; }
}
CreditCardStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class CreditCardStrategy implements PaymentStrategy {
private final String cardNumber;
private final String cvv;
private final String expiry;
public CreditCardStrategy(String cardNumber, String cvv, String expiry) {
this.cardNumber = cardNumber;
this.cvv = cvv;
this.expiry = expiry;
}
@Override
public boolean validate() {
return cardNumber != null && cardNumber.length() == 16
&& cvv != null && cvv.length() == 3
&& expiry != null && !expiry.isEmpty();
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Invalid card details");
}
String masked = "****-" + cardNumber.substring(12);
System.out.println(" [CARD] Authorizing card " + masked);
System.out.println(" [CARD] Charging Rs " + amount);
System.out.println(" [CARD] Payment settled");
return PaymentResult.success("Paid Rs " + amount + " via Credit Card (" + masked + ")");
}
@Override
public String getName() { return "Credit Card"; }
}
NetBankingStrategy.java
package com.nbi.shop.strategy;
import com.nbi.shop.model.PaymentResult;
import java.math.BigDecimal;
public class NetBankingStrategy implements PaymentStrategy {
private final String bankCode;
public NetBankingStrategy(String bankCode) {
this.bankCode = bankCode;
}
@Override
public boolean validate() {
return bankCode != null && !bankCode.isEmpty();
}
@Override
public PaymentResult pay(BigDecimal amount) {
if (!validate()) {
return PaymentResult.failure("Bank code is required");
}
System.out.println(" [NETBANK] Redirecting to " + bankCode + " portal");
System.out.println(" [NETBANK] User approved transaction");
System.out.println(" [NETBANK] Debit confirmed");
return PaymentResult.success("Paid Rs " + amount + " via Net Banking (" + bankCode + ")");
}
@Override
public String getName() { return "Net Banking"; }
}
ShoppingCart.java (Context / Target class)
package com.nbi.shop.cart;
import com.nbi.shop.model.CartItem;
import com.nbi.shop.model.PaymentResult;
import com.nbi.shop.strategy.PaymentStrategy;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
private PaymentStrategy paymentStrategy; // HAS-A (composition)
public void addItem(CartItem item) {
items.add(item);
}
public void removeItem(String name) {
items.removeIf(item -> item.getName().equals(name));
}
public BigDecimal getTotal() {
return items.stream()
.map(CartItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
// Strategy injection via setter
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void displayCart() {
System.out.println("Cart Items:");
for (CartItem item : items) {
System.out.println(" " + item);
}
System.out.println(" Total: Rs " + getTotal());
}
public PaymentResult checkout() {
if (items.isEmpty()) {
return PaymentResult.failure("Cart is empty");
}
if (paymentStrategy == null) {
return PaymentResult.failure("No payment method selected");
}
BigDecimal total = getTotal();
System.out.println("Checking out with " + paymentStrategy.getName()
+ " | Amount: Rs " + total);
PaymentResult result = paymentStrategy.pay(total);
if (result.isSuccess()) {
items.clear();
}
return result;
}
}
Application.java
package com.nbi.shop;
import com.nbi.shop.cart.ShoppingCart;
import com.nbi.shop.model.CartItem;
import com.nbi.shop.model.PaymentResult;
import com.nbi.shop.strategy.*;
import java.math.BigDecimal;
public class Application {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Add items
cart.addItem(new CartItem("Java: The Complete Reference", new BigDecimal("799"), 1));
cart.addItem(new CartItem("Mechanical Keyboard", new BigDecimal("3500"), 1));
cart.addItem(new CartItem("USB-C Hub", new BigDecimal("1200"), 2));
// Scenario 1: Pay with UPI
System.out.println("=== Scenario 1: UPI Payment ===");
cart.displayCart();
cart.setPaymentStrategy(new UPIStrategy("rajesh@icici"));
PaymentResult r1 = cart.checkout();
System.out.println("Result: " + r1);
System.out.println();
// Scenario 2: New cart, pay with Credit Card
System.out.println("=== Scenario 2: Credit Card Payment ===");
cart.addItem(new CartItem("Monitor", new BigDecimal("15000"), 1));
cart.addItem(new CartItem("Mouse", new BigDecimal("800"), 1));
cart.displayCart();
cart.setPaymentStrategy(new CreditCardStrategy("4111222233334444", "123", "12/28"));
PaymentResult r2 = cart.checkout();
System.out.println("Result: " + r2);
System.out.println();
// Scenario 3: Switch strategy at runtime to Net Banking
System.out.println("=== Scenario 3: Net Banking (runtime switch) ===");
cart.addItem(new CartItem("Standing Desk", new BigDecimal("25000"), 1));
cart.displayCart();
cart.setPaymentStrategy(new NetBankingStrategy("SBI"));
PaymentResult r3 = cart.checkout();
System.out.println("Result: " + r3);
System.out.println();
// Scenario 4: Validation failure (invalid UPI)
System.out.println("=== Scenario 4: Invalid UPI ===");
cart.addItem(new CartItem("Book", new BigDecimal("500"), 1));
cart.setPaymentStrategy(new UPIStrategy("invalid_no_at"));
PaymentResult r4 = cart.checkout();
System.out.println("Result: " + r4);
System.out.println();
// Scenario 5: Empty cart
System.out.println("=== Scenario 5: Empty Cart ===");
ShoppingCart emptyCart = new ShoppingCart();
emptyCart.setPaymentStrategy(new UPIStrategy("test@bank"));
PaymentResult r5 = emptyCart.checkout();
System.out.println("Result: " + r5);
System.out.println();
// Scenario 6: No payment strategy set
System.out.println("=== Scenario 6: No Payment Method ===");
ShoppingCart noPayCart = new ShoppingCart();
noPayCart.addItem(new CartItem("Item", new BigDecimal("100"), 1));
PaymentResult r6 = noPayCart.checkout();
System.out.println("Result: " + r6);
}
}
Expected Output
Key observations from the project
- ShoppingCart never imports concrete strategy classes. It depends only on the
PaymentStrategyinterface. Composition, not inheritance. - Strategy is swapped at runtime via
setPaymentStrategy(). Scenario 1 uses UPI, scenario 2 switches to Credit Card, scenario 3 switches to Net Banking. The ShoppingCart code does not change. - Adding a Wallet strategy: Create
WalletStrategy implements PaymentStrategy, inject it viasetPaymentStrategy(new WalletStrategy()). Zero changes to ShoppingCart or other strategies. - Each strategy validates independently. UPI checks for @ in ID. Credit Card checks 16-digit number. Net Banking checks bank code. The cart does not know the validation rules.
- Six scenarios cover happy path, runtime switch, validation failure, empty cart, and missing strategy. All demonstrate loose coupling.
- Spring equivalent: Replace
setPaymentStrategy()with@Autowired @Qualifier("upi")and the framework handles injection.
9. Strategy vs Template Method
| Aspect | Strategy | Template Method |
|---|---|---|
| Mechanism | Composition (HAS-A interface reference) | Inheritance (abstract class + subclasses) |
| What varies | The entire algorithm | Some steps of the algorithm |
| Algorithm locked? | No. Each strategy defines its own flow | Yes. Template method is final |
| Runtime swap | Yes. Change strategy object at runtime | No. Object type fixed at creation |
| Open/Closed | Add new strategy class. No existing changes. | Add new subclass. No existing changes. |
| Testing | Easy. Mock the interface. | Harder. Must test inherited + own methods. |
| Use when | Multiple interchangeable algorithms | Fixed algorithm with varying steps |
10. When to Use and When NOT to Use
When to use Strategy
| Scenario | Example | Why Strategy |
|---|---|---|
| Multiple payment methods | UPI, Card, NetBanking, Wallet | Customer picks at runtime. Cart logic unchanged. |
| Sorting algorithms | BubbleSort, QuickSort, MergeSort | Choose based on data size. Swap without changing caller. |
| Compression | ZIP, GZIP, LZ4 | Choose based on file type. All implement same interface. |
| Authentication | JWT, OAuth2, Basic Auth | Different systems use different auth. Strategy lets you swap. |
| Notification channels | Email, SMS, Push, Slack | User preference determines channel. Inject at runtime. |
When NOT to use Strategy
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Only one algorithm | No alternatives to swap between | Direct implementation |
| Algorithm never changes at runtime | If decided at compile time, strategy adds unnecessary indirection | Template Method or simple if/else |
| Too many strategies | If strategies proliferate and each is trivial, the pattern adds class count for little benefit | Lambda expressions or functional approach |
11. Quick Reference Card
| Component | Role | In Shopping Cart Project |
|---|---|---|
| Context (Target) | Uses strategy via interface reference | ShoppingCart |
| Strategy Interface | Common contract for all algorithms | PaymentStrategy |
| Concrete Strategies | Each implements one algorithm | UPIStrategy, CreditCardStrategy, NetBankingStrategy |
| Client | Creates strategy, injects into context | Application |
| Injection method | Setter or constructor | setPaymentStrategy() |