Strategy Pattern

Introduction

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. Instead of hardcoding behavior into a single class with conditionals, you extract each algorithm into its own class and let the client choose which one to use.

This is one of the most practical and frequently used design patterns in real-world software. If you have ever swapped out a payment method at checkout or changed a sorting algorithm based on data size, you have seen the Strategy Pattern in action.

The Problem

Imagine you are building a payment processing system. Customers can pay with credit cards, PayPal, or cryptocurrency. The naive approach is to write a giant if-else or switch block inside a single method:

“If credit card, do X. If PayPal, do Y. If crypto, do Z.”

This works initially, but every time you add a new payment method (Apple Pay, bank transfer, etc.), you must modify the existing class. This violates the Open/Closed Principle, increases the risk of bugs, and makes the code harder to test because every payment path lives in the same method.

The Solution

The Strategy Pattern solves this by extracting each payment algorithm into its own class that implements a common interface. A context class (the checkout processor) holds a reference to the current strategy and delegates the payment call to it. Adding a new payment method means creating a new class — no existing code changes required.

The context does not care how the payment is processed. It only knows that the strategy it holds can pay(). This decouples the “what” from the “how.”

Key Principle

Program to an interface, not an implementation. By depending on an abstraction (the strategy interface) rather than concrete payment classes, the system becomes flexible, testable, and open for extension without modification.

Java Example

Scenario: A checkout system that supports multiple payment strategies.

// Strategy interface
public interface PaymentStrategy {
    void pay(double amount);
    String getPaymentMethod();
}

// Concrete Strategy: Credit Card
public class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;
    private final String cardHolder;

    public CreditCardPayment(String cardNumber, String cardHolder) {
        this.cardNumber = cardNumber;
        this.cardHolder = cardHolder;
    }

    @Override
    public void pay(double amount) {
        String masked = "****-" + cardNumber.substring(cardNumber.length() - 4);
        System.out.printf("Charged $%.2f to credit card %s (%s)%n",
            amount, masked, cardHolder);
    }

    @Override
    public String getPaymentMethod() {
        return "Credit Card";
    }
}

// Concrete Strategy: PayPal
public class PayPalPayment implements PaymentStrategy {
    private final String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(double amount) {
        System.out.printf("Transferred $%.2f via PayPal to %s%n", amount, email);
    }

    @Override
    public String getPaymentMethod() {
        return "PayPal";
    }
}

// Concrete Strategy: Cryptocurrency
public class CryptoPayment implements PaymentStrategy {
    private final String walletAddress;

    public CryptoPayment(String walletAddress) {
        this.walletAddress = walletAddress;
    }

    @Override
    public void pay(double amount) {
        System.out.printf("Sent $%.2f in crypto to wallet %s%n",
            amount, walletAddress);
    }

    @Override
    public String getPaymentMethod() {
        return "Cryptocurrency";
    }
}

// Context
public class CheckoutProcessor {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(double amount) {
        if (paymentStrategy == null) {
            throw new IllegalStateException("Payment strategy not set");
        }
        System.out.println("Processing checkout...");
        System.out.println("Payment method: " + paymentStrategy.getPaymentMethod());
        paymentStrategy.pay(amount);
        System.out.println("Checkout complete.\n");
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        CheckoutProcessor processor = new CheckoutProcessor();

        // Pay with credit card
        processor.setPaymentStrategy(
            new CreditCardPayment("4111111111111234", "John Doe"));
        processor.checkout(99.99);

        // Switch to PayPal at runtime
        processor.setPaymentStrategy(
            new PayPalPayment("john@example.com"));
        processor.checkout(49.50);

        // Switch to crypto
        processor.setPaymentStrategy(
            new CryptoPayment("0xABC123DEF456"));
        processor.checkout(250.00);
    }
}

Python Example

Same scenario in Python, using abstract base classes and duck typing.

from abc import ABC, abstractmethod


# Strategy interface
class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None:
        pass

    @abstractmethod
    def get_payment_method(self) -> str:
        pass


# Concrete Strategy: Credit Card
class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str, card_holder: str):
        self.card_number = card_number
        self.card_holder = card_holder

    def pay(self, amount: float) -> None:
        masked = f"****-{self.card_number[-4:]}"
        print(f"Charged ${amount:.2f} to credit card {masked} ({self.card_holder})")

    def get_payment_method(self) -> str:
        return "Credit Card"


# Concrete Strategy: PayPal
class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def pay(self, amount: float) -> None:
        print(f"Transferred ${amount:.2f} via PayPal to {self.email}")

    def get_payment_method(self) -> str:
        return "PayPal"


# Concrete Strategy: Cryptocurrency
class CryptoPayment(PaymentStrategy):
    def __init__(self, wallet_address: str):
        self.wallet_address = wallet_address

    def pay(self, amount: float) -> None:
        print(f"Sent ${amount:.2f} in crypto to wallet {self.wallet_address}")

    def get_payment_method(self) -> str:
        return "Cryptocurrency"


# Context
class CheckoutProcessor:
    def __init__(self):
        self._payment_strategy: PaymentStrategy | None = None

    def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
        self._payment_strategy = strategy

    def checkout(self, amount: float) -> None:
        if self._payment_strategy is None:
            raise RuntimeError("Payment strategy not set")
        print("Processing checkout...")
        print(f"Payment method: {self._payment_strategy.get_payment_method()}")
        self._payment_strategy.pay(amount)
        print("Checkout complete.\n")


# Usage
if __name__ == "__main__":
    processor = CheckoutProcessor()

    # Pay with credit card
    processor.set_payment_strategy(
        CreditCardPayment("4111111111111234", "John Doe"))
    processor.checkout(99.99)

    # Switch to PayPal at runtime
    processor.set_payment_strategy(
        PayPalPayment("john@example.com"))
    processor.checkout(49.50)

    # Switch to crypto
    processor.set_payment_strategy(
        CryptoPayment("0xABC123DEF456"))
    processor.checkout(250.00)

When to Use

  • Multiple algorithms for the same task — When you have several ways to perform an operation (sorting, compression, payment, etc.) and want to switch between them.
  • Eliminating conditionals — When a method contains large if-else or switch blocks that choose between behaviors based on type or configuration.
  • Runtime flexibility — When the algorithm needs to be selected or changed at runtime based on user input, configuration, or context.
  • Isolated testing — When you want to unit test each algorithm independently without testing the entire context class.

Real-World Usage

  • Java Collections ComparatorCollections.sort(list, comparator) lets you pass different sorting strategies.
  • Spring Security AuthenticationProvider — Different authentication strategies (LDAP, OAuth, database) implement the same interface.
  • Python sorted() with key — The key parameter is a strategy function that determines sort order.
  • Stripe/Payment Gateways — Payment SDKs use strategy-like patterns to handle different payment method types behind a unified API.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *