Strategy Pattern

intermediate 2-4 YOE lld design-pattern behavioral

The Strategy pattern lets us define a family of algorithms, put each one in its own class, and swap them at runtime. The client code doesn’t change at all — it just picks a different strategy.

Think of it like a navigation app. We want to go from A to B. The app gives us options: fastest route, shortest route, avoid tolls, walking only. Same destination, same app — but completely different algorithms calculating the path. That’s Strategy.

The Problem

Imagine we’re building a payment system. Without Strategy, our code looks like this:

if paymentType == "credit_card":
    # 30 lines of credit card logic
elif paymentType == "paypal":
    # 25 lines of PayPal logic
elif paymentType == "crypto":
    # 35 lines of crypto logic
elif paymentType == "bank_transfer":
    # 20 lines of bank transfer logic

Every new payment method means another elif block. The class gets bigger and bigger. Testing is painful. Adding UPI means touching this giant file. This violates both Single Responsibility and Open/Closed principles.

How Strategy Fixes This

Strategy Pattern Structure
Context (PaymentProcessor)
- strategy: PaymentStrategy
- pay(amount)
│ delegates to
PaymentStrategy (Interface)
+ execute(amount)
│ implemented by
CreditCardStrategy
PayPalStrategy
CryptoStrategy

Each algorithm gets its own class. The context (PaymentProcessor) holds a reference to a strategy and delegates to it. We can swap strategies at runtime without touching any existing code.

Implementation

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def execute(self, amount: float) -> None:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self.card_number = card_number

    def execute(self, amount: float):
        print(f"Charged ${amount:.2f} to card ending {self.card_number[-4:]}")

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self.email = email

    def execute(self, amount: float):
        print(f"Sent ${amount:.2f} via PayPal to {self.email}")

class CryptoPayment(PaymentStrategy):
    def __init__(self, wallet_address: str):
        self.wallet = wallet_address

    def execute(self, amount: float):
        print(f"Transferred ${amount:.2f} in crypto to {self.wallet[:8]}...")

class PaymentProcessor:
    def __init__(self, strategy: PaymentStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: PaymentStrategy):
        self._strategy = strategy

    def pay(self, amount: float):
        self._strategy.execute(amount)

# Usage
processor = PaymentProcessor(CreditCardPayment("4111111111111234"))
processor.pay(99.99)  # Charged $99.99 to card ending 1234

processor.set_strategy(PayPalPayment("dev@example.com"))
processor.pay(49.99)  # Sent $49.99 via PayPal to dev@example.com

processor.set_strategy(CryptoPayment("0xABCDEF1234567890"))
processor.pay(199.99)  # Transferred $199.99 in crypto to 0xABCDEF...
class CreditCardPayment {
  constructor(cardNumber) {
    this.cardNumber = cardNumber;
  }
  execute(amount) {
    console.log(`Charged $${amount.toFixed(2)} to card ending ${this.cardNumber.slice(-4)}`);
  }
}

class PayPalPayment {
  constructor(email) {
    this.email = email;
  }
  execute(amount) {
    console.log(`Sent $${amount.toFixed(2)} via PayPal to ${this.email}`);
  }
}

class CryptoPayment {
  constructor(walletAddress) {
    this.wallet = walletAddress;
  }
  execute(amount) {
    console.log(`Transferred $${amount.toFixed(2)} in crypto to ${this.wallet.slice(0, 8)}...`);
  }
}

class PaymentProcessor {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  pay(amount) {
    this.strategy.execute(amount);
  }
}

// Usage
const processor = new PaymentProcessor(new CreditCardPayment("4111111111111234"));
processor.pay(99.99);

processor.setStrategy(new PayPalPayment("dev@example.com"));
processor.pay(49.99);
interface PaymentStrategy {
    void execute(double amount);
}

class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;

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

    public void execute(double amount) {
        System.out.printf("Charged $%.2f to card ending %s%n",
            amount, cardNumber.substring(cardNumber.length() - 4));
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

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

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

class CryptoPayment implements PaymentStrategy {
    private String wallet;

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

    public void execute(double amount) {
        System.out.printf("Transferred $%.2f in crypto to %s...%n",
            amount, wallet.substring(0, 8));
    }
}

class PaymentProcessor {
    private PaymentStrategy strategy;

    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

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

    public void pay(double amount) {
        strategy.execute(amount);
    }
}

When to Use

  • Multiple algorithms for the same task — sorting, compression, validation, pricing
  • Eliminating conditional logic — replacing if-else chains with polymorphism
  • Runtime behavior switching — user picks a shipping method, discount type, etc.
  • Testing — we can inject mock strategies easily

When NOT to Use

  • When we only have 2 algorithms that will never change — a simple if-else is fine
  • When the algorithms share a lot of state with the context — the separation becomes awkward
  • When clients don’t need to know about different strategies — added complexity for no gain

Strategy vs State Pattern

People mix these up constantly in interviews. The key difference:

  • Strategy — the client picks which algorithm to use. Strategies don’t know about each other.
  • State — the object transitions between states on its own. States know which state comes next.

The only difference is who controls the switching. In Strategy, it’s the outside code. In State, it’s the states themselves.

In simple language, Strategy is like having a toolbox. We pick the right tool for the job and snap it in. Need a different tool? Swap it out. The workbench (context) doesn’t care which tool we’re using — it just says “do the thing” and the tool handles the rest.