Mediumbehavioral18 min read

Strategy Design Pattern

1. What is Strategy Pattern?

Definition

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 ClassRelationshipDependent Class
Flipkartuses/needsDTDC, BlueDart, DHL (Courier Services)
Vehicleuses/needsEngine
Cricketoruses/needsCricketBat

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

Three Rules
  1. Favour composition over inheritance
  2. Code to interfaces/abstract classes, never code to implementation classes (concrete classes)
  3. 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.

Problem (with inheritance): class A class B class C extends A, B ... ... ... (Definitely not possible in Java!) Solution (using composition): class A class B class C { ... ... private A a; // HAS-A private B b; // HAS-A }

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"
Solution using composition
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

Unit Testing: Programmers testing on his own piece code is called unit testing. If expected results are matching with actual results, then we say test results are positive. If not matching, we say test results are negative. class A { class B extends A { 10 methods ... other 10 methods ... ... } } // Inside class: // B knows A's: // total B = 10 (own) + 10 (from A) = 20 methods // We need to perform unit testing: 10 methods while testing A class (10 methods to test) // But while testing B class: 20 methods to test (inherited + own) Solution using composition: class A { class B { 10 methods private A a = new A(); ... int x = a.m1(); } ... other 9 methods } // Now: Unit testing required only on 10 methods (B's own). // While doing B class methods, some of the A class methods will be tested. // Less methods to involve in unit testing!

4. Rule b) Code to Interfaces, Not Concrete Classes

Problem: Tight Coupling
// 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.

Solution: Loose Coupling
// 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/Closed Principle
  • 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

ComponentRoleIn Our Example
Context (Target)The class that uses a strategy. Holds interface reference.Communication
Strategy InterfaceCommon interface for all algorithms.Courier
Concrete StrategiesMultiple implementations of the interface.DHL, BlueDart, DTDC
ClientCreates strategy and injects it into context.ClientApp
Client (injects strategy into target class) | +-- Target Class: Communication | private Courier courier; (HAS-A interface ref) | setCourier(Courier c); (setter injection) | shipping(); (uses courier.deliver()) | +-- Strategy Interface: Courier | +----+----+----+ | | | | DHL BlueDart DTDC (future: FedEx, Ekart...)

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);
    }
}
=== Using DHL === DHL:: 0-param constructor Processing shipment of 2 items... Item: Java Book | Price: Rs 500.0 Item: Laptop Stand | Price: Rs 1200.0 Total: Rs 1700.0 DHL delivering order #1001 via express air Estimate: 2-3 business days to Hyderabad === Switching to BlueDart === BlueDart:: 0-param constructor Processing shipment of 2 items... Item: Java Book | Price: Rs 500.0 Item: Laptop Stand | Price: Rs 1200.0 Total: Rs 1700.0 BlueDart delivering order #1001 via ground Estimate: 4-5 business days to Hyderabad
Key Observation

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

Connection to Spring

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

Business Scenario

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)

+---------------------------------------------------+ | CLIENT LAYER | | Application (main) | | +-- creates ShoppingCart (context) | | +-- adds items to cart | | +-- selects payment strategy at checkout | +----------------------------+----------------------+ | v +---------------------------------------------------+ | CONTEXT LAYER | | ShoppingCart (target class) | | - items: List<CartItem> | | - paymentStrategy: PaymentStrategy (HAS-A) | | + addItem(item) | | + setPaymentStrategy(strategy) | | + checkout(): PaymentResult | +---------------------------------------------------+ | v +---------------------------------------------------+ | STRATEGY LAYER | | PaymentStrategy (interface) | | + pay(amount): PaymentResult | | + getName(): String | | | | | +----+--------+----------+ | | | | | | | UPI CreditCard NetBanking | | Strategy Strategy Strategy | +---------------------------------------------------+ | v +---------------------------------------------------+ | MODEL LAYER | | CartItem: name, price, quantity | | PaymentResult: success, txnId, message | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | PaymentStrategy (interface) | +-------------------------------------------------------+ | + pay(amount: BigDecimal): PaymentResult | | + getName(): String | | + validate(): boolean | +-------------------------------------------------------+ ^ ^ ^ | | | +-----------------+ +------------------+ +-------------------+ | UPIStrategy | | CreditCardStrat | | NetBankingStrat | +-----------------+ +------------------+ +-------------------+ | - upiId: String | | - cardNumber | | - bankCode | | | | - cvv, expiry | | - userId | +-----------------+ +------------------+ +-------------------+ +-------------------------------------------------------+ | ShoppingCart (Context) | +-------------------------------------------------------+ | - items: List<CartItem> | | - paymentStrategy: PaymentStrategy [HAS-A interface] | +-------------------------------------------------------+ | + addItem(item): void | | + removeItem(name): void | | + getTotal(): BigDecimal | | + setPaymentStrategy(strategy): void | | + checkout(): PaymentResult | | + displayCart(): void | +-------------------------------------------------------+

Project Structure

nbi-shopping-cart/ ├── src/ │ └── main/java/com/nbi/shop/ │ ├── model/ │ │ ├── CartItem.java │ │ └── PaymentResult.java │ ├── strategy/ │ │ ├── PaymentStrategy.java (interface) │ │ ├── UPIStrategy.java │ │ ├── CreditCardStrategy.java │ │ └── NetBankingStrategy.java │ ├── cart/ │ │ └── ShoppingCart.java (context) │ └── Application.java ├── src/ │ └── test/java/com/nbi/shop/ │ ├── strategy/ │ │ └── PaymentStrategyTest.java │ └── cart/ │ └── ShoppingCartTest.java └── pom.xml

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

=== Scenario 1: UPI Payment === Cart Items: Java: The Complete Reference x1 @ Rs 799 = Rs 799 Mechanical Keyboard x1 @ Rs 3500 = Rs 3500 USB-C Hub x2 @ Rs 1200 = Rs 2400 Total: Rs 6699 Checking out with UPI | Amount: Rs 6699 [UPI] Sending request to rajesh@icici [UPI] Waiting for approval... [UPI] Payment approved Result: SUCCESS | TXN-A1B2C3D4 | Paid Rs 6699 via UPI (rajesh@icici) === Scenario 2: Credit Card Payment === Cart Items: Monitor x1 @ Rs 15000 = Rs 15000 Mouse x1 @ Rs 800 = Rs 800 Total: Rs 15800 Checking out with Credit Card | Amount: Rs 15800 [CARD] Authorizing card ****-4444 [CARD] Charging Rs 15800 [CARD] Payment settled Result: SUCCESS | TXN-E5F6G7H8 | Paid Rs 15800 via Credit Card (****-4444) === Scenario 3: Net Banking (runtime switch) === Cart Items: Standing Desk x1 @ Rs 25000 = Rs 25000 Total: Rs 25000 Checking out with Net Banking | Amount: Rs 25000 [NETBANK] Redirecting to SBI portal [NETBANK] User approved transaction [NETBANK] Debit confirmed Result: SUCCESS | TXN-I9J0K1L2 | Paid Rs 25000 via Net Banking (SBI) === Scenario 4: Invalid UPI === Checking out with UPI | Amount: Rs 500 Result: FAILED | Invalid UPI ID: invalid_no_at === Scenario 5: Empty Cart === Result: FAILED | Cart is empty === Scenario 6: No Payment Method === Result: FAILED | No payment method selected

Key observations from the project

  1. ShoppingCart never imports concrete strategy classes. It depends only on the PaymentStrategy interface. Composition, not inheritance.
  2. 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.
  3. Adding a Wallet strategy: Create WalletStrategy implements PaymentStrategy, inject it via setPaymentStrategy(new WalletStrategy()). Zero changes to ShoppingCart or other strategies.
  4. 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.
  5. Six scenarios cover happy path, runtime switch, validation failure, empty cart, and missing strategy. All demonstrate loose coupling.
  6. Spring equivalent: Replace setPaymentStrategy() with @Autowired @Qualifier("upi") and the framework handles injection.

9. Strategy vs Template Method

AspectStrategyTemplate Method
MechanismComposition (HAS-A interface reference)Inheritance (abstract class + subclasses)
What variesThe entire algorithmSome steps of the algorithm
Algorithm locked?No. Each strategy defines its own flowYes. Template method is final
Runtime swapYes. Change strategy object at runtimeNo. Object type fixed at creation
Open/ClosedAdd new strategy class. No existing changes.Add new subclass. No existing changes.
TestingEasy. Mock the interface.Harder. Must test inherited + own methods.
Use whenMultiple interchangeable algorithmsFixed algorithm with varying steps

10. When to Use and When NOT to Use

When to use Strategy

ScenarioExampleWhy Strategy
Multiple payment methodsUPI, Card, NetBanking, WalletCustomer picks at runtime. Cart logic unchanged.
Sorting algorithmsBubbleSort, QuickSort, MergeSortChoose based on data size. Swap without changing caller.
CompressionZIP, GZIP, LZ4Choose based on file type. All implement same interface.
AuthenticationJWT, OAuth2, Basic AuthDifferent systems use different auth. Strategy lets you swap.
Notification channelsEmail, SMS, Push, SlackUser preference determines channel. Inject at runtime.

When NOT to use Strategy

ScenarioWhy NOTUse Instead
Only one algorithmNo alternatives to swap betweenDirect implementation
Algorithm never changes at runtimeIf decided at compile time, strategy adds unnecessary indirectionTemplate Method or simple if/else
Too many strategiesIf strategies proliferate and each is trivial, the pattern adds class count for little benefitLambda expressions or functional approach

11. Quick Reference Card

ComponentRoleIn Shopping Cart Project
Context (Target)Uses strategy via interface referenceShoppingCart
Strategy InterfaceCommon contract for all algorithmsPaymentStrategy
Concrete StrategiesEach implements one algorithmUPIStrategy, CreditCardStrategy, NetBankingStrategy
ClientCreates strategy, injects into contextApplication
Injection methodSetter or constructorsetPaymentStrategy()
Behavioral GOF Pattern Medium

Strategy Design Pattern in Java

Complete Tutorial | 12 Sections | Full Shopping Cart Project with HLD and LLD | Three Rules of Strategy | Composition over Inheritance | Spring DI Connection | Multiple Scenarios | 12 Interview Questions

1. What is Strategy Pattern?

Definition

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 ClassRelationshipDependent Class
Flipkartuses/needsDTDC, BlueDart, DHL (Courier Services)
Vehicleuses/needsEngine
Cricketoruses/needsCricketBat

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

Three Rules
  1. Favour composition over inheritance
  2. Code to interfaces/abstract classes, never code to implementation classes (concrete classes)
  3. 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.

Problem (with inheritance): class A class B class C extends A, B ... ... ... (Definitely not possible in Java!) Solution (using composition): class A class B class C { ... ... private A a; // HAS-A private B b; // HAS-A }

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"
Solution using composition
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

Unit Testing: Programmers testing on his own piece code is called unit testing. If expected results are matching with actual results, then we say test results are positive. If not matching, we say test results are negative. class A { class B extends A { 10 methods ... other 10 methods ... ... } } // Inside class: // B knows A's: // total B = 10 (own) + 10 (from A) = 20 methods // We need to perform unit testing: 10 methods while testing A class (10 methods to test) // But while testing B class: 20 methods to test (inherited + own) Solution using composition: class A { class B { 10 methods private A a = new A(); ... int x = a.m1(); } ... other 9 methods } // Now: Unit testing required only on 10 methods (B's own). // While doing B class methods, some of the A class methods will be tested. // Less methods to involve in unit testing!

4. Rule b) Code to Interfaces, Not Concrete Classes

Problem: Tight Coupling
// 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.

Solution: Loose Coupling
// 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/Closed Principle
  • 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

ComponentRoleIn Our Example
Context (Target)The class that uses a strategy. Holds interface reference.Communication
Strategy InterfaceCommon interface for all algorithms.Courier
Concrete StrategiesMultiple implementations of the interface.DHL, BlueDart, DTDC
ClientCreates strategy and injects it into context.ClientApp
Client (injects strategy into target class) | +-- Target Class: Communication | private Courier courier; (HAS-A interface ref) | setCourier(Courier c); (setter injection) | shipping(); (uses courier.deliver()) | +-- Strategy Interface: Courier | +----+----+----+ | | | | DHL BlueDart DTDC (future: FedEx, Ekart...)

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);
    }
}
=== Using DHL === DHL:: 0-param constructor Processing shipment of 2 items... Item: Java Book | Price: Rs 500.0 Item: Laptop Stand | Price: Rs 1200.0 Total: Rs 1700.0 DHL delivering order #1001 via express air Estimate: 2-3 business days to Hyderabad === Switching to BlueDart === BlueDart:: 0-param constructor Processing shipment of 2 items... Item: Java Book | Price: Rs 500.0 Item: Laptop Stand | Price: Rs 1200.0 Total: Rs 1700.0 BlueDart delivering order #1001 via ground Estimate: 4-5 business days to Hyderabad
Key Observation

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

Connection to Spring

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

Business Scenario

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)

+---------------------------------------------------+ | CLIENT LAYER | | Application (main) | | +-- creates ShoppingCart (context) | | +-- adds items to cart | | +-- selects payment strategy at checkout | +----------------------------+----------------------+ | v +---------------------------------------------------+ | CONTEXT LAYER | | ShoppingCart (target class) | | - items: List<CartItem> | | - paymentStrategy: PaymentStrategy (HAS-A) | | + addItem(item) | | + setPaymentStrategy(strategy) | | + checkout(): PaymentResult | +---------------------------------------------------+ | v +---------------------------------------------------+ | STRATEGY LAYER | | PaymentStrategy (interface) | | + pay(amount): PaymentResult | | + getName(): String | | | | | +----+--------+----------+ | | | | | | | UPI CreditCard NetBanking | | Strategy Strategy Strategy | +---------------------------------------------------+ | v +---------------------------------------------------+ | MODEL LAYER | | CartItem: name, price, quantity | | PaymentResult: success, txnId, message | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | PaymentStrategy (interface) | +-------------------------------------------------------+ | + pay(amount: BigDecimal): PaymentResult | | + getName(): String | | + validate(): boolean | +-------------------------------------------------------+ ^ ^ ^ | | | +-----------------+ +------------------+ +-------------------+ | UPIStrategy | | CreditCardStrat | | NetBankingStrat | +-----------------+ +------------------+ +-------------------+ | - upiId: String | | - cardNumber | | - bankCode | | | | - cvv, expiry | | - userId | +-----------------+ +------------------+ +-------------------+ +-------------------------------------------------------+ | ShoppingCart (Context) | +-------------------------------------------------------+ | - items: List<CartItem> | | - paymentStrategy: PaymentStrategy [HAS-A interface] | +-------------------------------------------------------+ | + addItem(item): void | | + removeItem(name): void | | + getTotal(): BigDecimal | | + setPaymentStrategy(strategy): void | | + checkout(): PaymentResult | | + displayCart(): void | +-------------------------------------------------------+

Project Structure

nbi-shopping-cart/ ├── src/ │ └── main/java/com/nbi/shop/ │ ├── model/ │ │ ├── CartItem.java │ │ └── PaymentResult.java │ ├── strategy/ │ │ ├── PaymentStrategy.java (interface) │ │ ├── UPIStrategy.java │ │ ├── CreditCardStrategy.java │ │ └── NetBankingStrategy.java │ ├── cart/ │ │ └── ShoppingCart.java (context) │ └── Application.java ├── src/ │ └── test/java/com/nbi/shop/ │ ├── strategy/ │ │ └── PaymentStrategyTest.java │ └── cart/ │ └── ShoppingCartTest.java └── pom.xml

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

=== Scenario 1: UPI Payment === Cart Items: Java: The Complete Reference x1 @ Rs 799 = Rs 799 Mechanical Keyboard x1 @ Rs 3500 = Rs 3500 USB-C Hub x2 @ Rs 1200 = Rs 2400 Total: Rs 6699 Checking out with UPI | Amount: Rs 6699 [UPI] Sending request to rajesh@icici [UPI] Waiting for approval... [UPI] Payment approved Result: SUCCESS | TXN-A1B2C3D4 | Paid Rs 6699 via UPI (rajesh@icici) === Scenario 2: Credit Card Payment === Cart Items: Monitor x1 @ Rs 15000 = Rs 15000 Mouse x1 @ Rs 800 = Rs 800 Total: Rs 15800 Checking out with Credit Card | Amount: Rs 15800 [CARD] Authorizing card ****-4444 [CARD] Charging Rs 15800 [CARD] Payment settled Result: SUCCESS | TXN-E5F6G7H8 | Paid Rs 15800 via Credit Card (****-4444) === Scenario 3: Net Banking (runtime switch) === Cart Items: Standing Desk x1 @ Rs 25000 = Rs 25000 Total: Rs 25000 Checking out with Net Banking | Amount: Rs 25000 [NETBANK] Redirecting to SBI portal [NETBANK] User approved transaction [NETBANK] Debit confirmed Result: SUCCESS | TXN-I9J0K1L2 | Paid Rs 25000 via Net Banking (SBI) === Scenario 4: Invalid UPI === Checking out with UPI | Amount: Rs 500 Result: FAILED | Invalid UPI ID: invalid_no_at === Scenario 5: Empty Cart === Result: FAILED | Cart is empty === Scenario 6: No Payment Method === Result: FAILED | No payment method selected

Key observations from the project

  1. ShoppingCart never imports concrete strategy classes. It depends only on the PaymentStrategy interface. Composition, not inheritance.
  2. 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.
  3. Adding a Wallet strategy: Create WalletStrategy implements PaymentStrategy, inject it via setPaymentStrategy(new WalletStrategy()). Zero changes to ShoppingCart or other strategies.
  4. 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.
  5. Six scenarios cover happy path, runtime switch, validation failure, empty cart, and missing strategy. All demonstrate loose coupling.
  6. Spring equivalent: Replace setPaymentStrategy() with @Autowired @Qualifier("upi") and the framework handles injection.

9. Strategy vs Template Method

AspectStrategyTemplate Method
MechanismComposition (HAS-A interface reference)Inheritance (abstract class + subclasses)
What variesThe entire algorithmSome steps of the algorithm
Algorithm locked?No. Each strategy defines its own flowYes. Template method is final
Runtime swapYes. Change strategy object at runtimeNo. Object type fixed at creation
Open/ClosedAdd new strategy class. No existing changes.Add new subclass. No existing changes.
TestingEasy. Mock the interface.Harder. Must test inherited + own methods.
Use whenMultiple interchangeable algorithmsFixed algorithm with varying steps

10. When to Use and When NOT to Use

When to use Strategy

ScenarioExampleWhy Strategy
Multiple payment methodsUPI, Card, NetBanking, WalletCustomer picks at runtime. Cart logic unchanged.
Sorting algorithmsBubbleSort, QuickSort, MergeSortChoose based on data size. Swap without changing caller.
CompressionZIP, GZIP, LZ4Choose based on file type. All implement same interface.
AuthenticationJWT, OAuth2, Basic AuthDifferent systems use different auth. Strategy lets you swap.
Notification channelsEmail, SMS, Push, SlackUser preference determines channel. Inject at runtime.

When NOT to use Strategy

ScenarioWhy NOTUse Instead
Only one algorithmNo alternatives to swap betweenDirect implementation
Algorithm never changes at runtimeIf decided at compile time, strategy adds unnecessary indirectionTemplate Method or simple if/else
Too many strategiesIf strategies proliferate and each is trivial, the pattern adds class count for little benefitLambda expressions or functional approach

11. Quick Reference Card

ComponentRoleIn Shopping Cart Project
Context (Target)Uses strategy via interface referenceShoppingCart
Strategy InterfaceCommon contract for all algorithmsPaymentStrategy
Concrete StrategiesEach implements one algorithmUPIStrategy, CreditCardStrategy, NetBankingStrategy
ClientCreates strategy, injects into contextApplication
Injection methodSetter or constructorsetPaymentStrategy()