Skip to main content

Strategy Pattern — Swappable Algorithms

Introduction

Picture a navigation app like Google Maps. You enter a destination and it offers multiple route options: driving, walking, cycling, public transit. The destination stays the same, but the algorithm used to compute the route is different for each mode.

Now imagine the code behind this. If the route calculation lives inside one giant method full of if/else branches — one for driving, one for walking, one for cycling — what happens when you need to add scooter routing? You crack open that method, add another branch, risk breaking existing logic, and violate the Open/Closed Principle from S6.

The Strategy Pattern eliminates this problem. It takes each algorithm (driving, walking, cycling) and encapsulates it in its own class behind a shared interface. The navigation app holds a reference to the current strategy and delegates the calculation to it — without knowing or caring which algorithm is active.

This is one of the most widely used behavioral patterns. You'll see it applied across 10+ design problems in the Design Problems course, including pricing strategies in Parking Lot and Hotel, scheduling algorithms in the Elevator System, and matching logic in Uber.

Real-World Analogy

Think about how you pay at a restaurant. You might pay with cash, credit card, or a mobile wallet. The restaurant doesn't care which method you use — it just needs the bill settled. Each payment method follows its own process internally (counting bills, swiping a card, scanning a QR code), but from the restaurant's perspective, the interaction is identical: "here's the amount, process the payment."

This is the Strategy Pattern in everyday life:

  • Context (the restaurant): Needs a task done but doesn't dictate how
  • Strategy interface ("process payment"): The common contract all methods agree to
  • Concrete strategies (cash, card, mobile): Different implementations of the same task

The restaurant can accept new payment methods (say cryptocurrency) by adding a new strategy — without retraining staff or changing the checkout process. That's the power of Strategy: new behaviors without changing existing code.

How It Works — Structure & Participants

The Strategy Pattern has three participants:

1. Strategy (Interface)
Defines the contract that all concrete algorithms must follow. It declares a method (or set of methods) that the context calls to execute the algorithm. In our navigation example, this would be a RouteStrategy interface with a calculate_route(origin, destination) method.

2. Concrete Strategies
Each class implements the Strategy interface with a specific algorithm. DrivingStrategy computes the fastest road route, WalkingStrategy finds pedestrian paths, CyclingStrategy picks bike-friendly roads. Each encapsulates its own logic. Adding a new algorithm means creating a new class — zero changes to existing code.

3. Context
The class that uses a strategy. It holds a reference to a Strategy object and delegates work to it. The context doesn't know which concrete strategy it's using — it only interacts through the interface. This is the NavigationApp that calls strategy.calculate_route() without knowing if driving or walking logic runs behind the scenes.

How they interact:

  • The client creates a concrete strategy and passes it to the context (via constructor or setter)
  • The context stores the strategy reference and calls the strategy's method when needed
  • The strategy executes its algorithm and returns the result
  • To switch algorithms, the client swaps the strategy object — the context code stays unchanged

UML Class Diagram

UML class diagram for Strategy Pattern showing Context holding a Strategy interface reference, with DrivingStrategy, WalkingStrategy, and CyclingStrategy as concrete implementations
UML class diagram for Strategy Pattern showing Context holding a Strategy interface reference, with DrivingStrategy, WalkingStrategy, and CyclingStrategy as concrete implementations

Visualization

Strategy Pattern — Swappable Algorithms in Action

Code Implementation

Let's implement the Strategy Pattern for a payment processing system. Different payment methods (credit card, PayPal, cryptocurrency) each have their own processing logic, but the checkout system treats them identically through a common interface.

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


@dataclass
class PaymentResult:
    """Holds the outcome of a payment attempt."""
    success: bool
    transaction_id: str
    message: str


class PaymentStrategy(ABC):
    """Strategy interface — defines how all payment methods must behave."""

    @abstractmethod
    def pay(self, amount: float) -> PaymentResult:
        """Process a payment for the given amount."""
        pass

    @abstractmethod
    def validate(self) -> bool:
        """Check if payment details are valid before processing."""
        pass


class CreditCardPayment(PaymentStrategy):
    """Concrete Strategy — processes payments via credit card."""

    def __init__(self, card_number: str, expiry: str, cvv: str):
        self._card_number = card_number
        self._expiry = expiry
        self._cvv = cvv

    def validate(self) -> bool:
        # Check card number length and expiry format
        if len(self._card_number) != 16 or not self._card_number.isdigit():
            return False
        if len(self._cvv) != 3 or not self._cvv.isdigit():
            return False
        return True

    def pay(self, amount: float) -> PaymentResult:
        if not self.validate():
            return PaymentResult(False, "", "Invalid card details")
        # In production, this calls a payment gateway API
        masked_card = f"****{self._card_number[-4:]}"
        return PaymentResult(
            success=True,
            transaction_id=f"CC-{id(self)}",
            message=f"Charged ${amount:.2f} to card {masked_card}"
        )


class PayPalPayment(PaymentStrategy):
    """Concrete Strategy — processes payments via PayPal."""

    def __init__(self, email: str):
        self._email = email

    def validate(self) -> bool:
        return "@" in self._email and "." in self._email

    def pay(self, amount: float) -> PaymentResult:
        if not self.validate():
            return PaymentResult(False, "", "Invalid PayPal email")
        return PaymentResult(
            success=True,
            transaction_id=f"PP-{id(self)}",
            message=f"Charged ${amount:.2f} to PayPal account {self._email}"
        )


class CryptoPayment(PaymentStrategy):
    """Concrete Strategy — processes payments via cryptocurrency."""

    def __init__(self, wallet_address: str, currency: str = "BTC"):
        self._wallet_address = wallet_address
        self._currency = currency

    def validate(self) -> bool:
        return len(self._wallet_address) >= 26

    def pay(self, amount: float) -> PaymentResult:
        if not self.validate():
            return PaymentResult(False, "", "Invalid wallet address")
        return PaymentResult(
            success=True,
            transaction_id=f"CRYPTO-{id(self)}",
            message=f"Sent ${amount:.2f} in {self._currency} to {self._wallet_address[:8]}..."
        )


class CheckoutService:
    """Context — uses a payment strategy to process orders."""

    def __init__(self, payment_strategy: PaymentStrategy):
        self._strategy = payment_strategy

    def set_payment_strategy(self, strategy: PaymentStrategy) -> None:
        """Switch the payment method at runtime."""
        self._strategy = strategy

    def checkout(self, amount: float) -> PaymentResult:
        """Process the payment using the current strategy."""
        print(f"Processing ${amount:.2f} payment...")
        result = self._strategy.pay(amount)
        if result.success:
            print(f"  ✓ {result.message} (ID: {result.transaction_id})")
        else:
            print(f"  ✗ Payment failed: {result.message}")
        return result


# --- Usage ---
# Create strategies
card = CreditCardPayment("4111111111111111", "12/26", "123")
paypal = PayPalPayment("user@example.com")
crypto = CryptoPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")

# Checkout with credit card
service = CheckoutService(card)
service.checkout(99.99)

# Switch to PayPal at runtime — no changes to CheckoutService
service.set_payment_strategy(paypal)
service.checkout(49.50)

# Switch to crypto
service.set_payment_strategy(crypto)
service.checkout(250.00)
// Strategy interface
interface PaymentStrategy {
    PaymentResult pay(double amount);
    boolean validate();
}

// Result class to hold payment outcome
class PaymentResult {
    private final boolean success;
    private final String transactionId;
    private final String message;

    public PaymentResult(boolean success, String transactionId, String message) {
        this.success = success;
        this.transactionId = transactionId;
        this.message = message;
    }

    public boolean isSuccess() { return success; }
    public String getTransactionId() { return transactionId; }
    public String getMessage() { return message; }
}

// Concrete Strategy — Credit Card
class CreditCardPayment implements PaymentStrategy {
    private final String cardNumber;
    private final String expiry;
    private final String cvv;

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

    @Override
    public boolean validate() {
        return cardNumber.length() == 16 && cardNumber.chars().allMatch(Character::isDigit)
                && cvv.length() == 3 && cvv.chars().allMatch(Character::isDigit);
    }

    @Override
    public PaymentResult pay(double amount) {
        if (!validate()) {
            return new PaymentResult(false, "", "Invalid card details");
        }
        String masked = "****" + cardNumber.substring(12);
        return new PaymentResult(true, "CC-" + hashCode(),
                String.format("Charged $%.2f to card %s", amount, masked));
    }
}

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

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

    @Override
    public boolean validate() {
        return email.contains("@") && email.contains(".");
    }

    @Override
    public PaymentResult pay(double amount) {
        if (!validate()) {
            return new PaymentResult(false, "", "Invalid PayPal email");
        }
        return new PaymentResult(true, "PP-" + hashCode(),
                String.format("Charged $%.2f to PayPal %s", amount, email));
    }
}

// Concrete Strategy — Cryptocurrency
class CryptoPayment implements PaymentStrategy {
    private final String walletAddress;
    private final String currency;

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

    @Override
    public boolean validate() {
        return walletAddress.length() >= 26;
    }

    @Override
    public PaymentResult pay(double amount) {
        if (!validate()) {
            return new PaymentResult(false, "", "Invalid wallet address");
        }
        return new PaymentResult(true, "CRYPTO-" + hashCode(),
                String.format("Sent $%.2f in %s to %s...", amount, currency,
                        walletAddress.substring(0, 8)));
    }
}

// Context — uses the strategy
class CheckoutService {
    private PaymentStrategy strategy;

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

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

    public PaymentResult checkout(double amount) {
        System.out.printf("Processing $%.2f payment...%n", amount);
        PaymentResult result = strategy.pay(amount);
        if (result.isSuccess()) {
            System.out.printf("  ✓ %s (ID: %s)%n", result.getMessage(), result.getTransactionId());
        } else {
            System.out.printf("  ✗ Payment failed: %s%n", result.getMessage());
        }
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        PaymentStrategy card = new CreditCardPayment("4111111111111111", "12/26", "123");
        PaymentStrategy paypal = new PayPalPayment("user@example.com");
        PaymentStrategy crypto = new CryptoPayment("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "BTC");

        CheckoutService service = new CheckoutService(card);
        service.checkout(99.99);

        // Switch strategy at runtime
        service.setPaymentStrategy(paypal);
        service.checkout(49.50);

        service.setPaymentStrategy(crypto);
        service.checkout(250.00);
    }
}

When to Use the Strategy Pattern

Use Strategy when:

  • Multiple algorithms solve the same problem differently: Sorting (quick sort, merge sort, bubble sort), compression (zip, gzip, brotli), routing (driving, walking, transit) — any scenario where the what stays constant but the how varies

  • You need runtime algorithm switching: The user selects a payment method at checkout, or the system adapts its pricing strategy based on time of day. The algorithm must be swappable without recompiling or redeploying

  • Conditional branching on algorithm type is growing: If you see if algorithm == 'A': ... elif algorithm == 'B': ... elif algorithm == 'C': ... — this is the Strategy Pattern begging to be applied. Each branch becomes its own class

  • You want to isolate algorithm logic for testing: Each strategy can be unit-tested independently. The context is tested with mock strategies. No tangled logic to untangle in tests

  • The Open/Closed Principle matters: New algorithms should be addable without modifying the context class or existing strategies. Strategy achieves exactly this

When NOT to Use the Strategy Pattern

Avoid Strategy when:

  • Only two fixed algorithms exist and won't change: If your system will only ever have two options (e.g., ascending vs descending sort) and requirements are frozen, a boolean flag is simpler. Strategy adds unnecessary indirection for trivial cases

  • The algorithm never changes at runtime: If the behavior is determined once at startup and never varies, Strategy's runtime-switching overhead provides no benefit. Consider configuring via dependency injection instead

  • The algorithms share significant state with the context: If every strategy needs access to 10 fields from the context object, passing all that data through the interface becomes awkward. At that point, the algorithm may belong inside the context

  • Clients must understand all strategies to choose correctly: Strategy assumes someone knows which strategy to select. If the selection logic itself is complex, consider combining Strategy with Factory (S10) to encapsulate the decision

Common Mistakes

Mistake 1: Strategy That Knows About the Context

The problem: A concrete strategy holds a reference back to the context and directly accesses its internal fields. This creates a circular dependency and defeats the pattern's purpose of decoupling.

The fix: Pass all required data through the strategy method's parameters. The strategy should be self-contained — given inputs, produce outputs. If a strategy needs too much context data, reconsider whether it truly belongs as a separate strategy.

Mistake 2: Fat Strategy Interface

The problem: The strategy interface has 8 methods, but most concrete strategies only use 2-3 of them. The rest throw NotImplementedError or return dummy values.

The fix: Keep strategy interfaces focused — ideally one primary method. If you need different groups of behavior, that signals multiple strategy interfaces, not one bloated one. This connects to the Interface Segregation Principle from S6.

Mistake 3: Hard-Coding Strategy Selection

The problem: The context creates its own strategy internally (self._strategy = CreditCardPayment(...)) instead of receiving it from outside. This eliminates the runtime-switching benefit entirely.

The fix: Inject the strategy through the constructor or a setter method. The context should never know concrete strategy types — it depends only on the interface. This follows the Dependency Inversion Principle (S6).

Mistake 4: Using Strategy When State Pattern Fits Better

The problem: You use Strategy to handle an object whose behavior changes based on its internal state (e.g., a vending machine that behaves differently when idle vs dispensing). You end up with external code manually switching strategies at every state transition.

The fix: If behavior changes are driven by internal state transitions, use the State Pattern (S12, Tutorial 4) instead. Strategy is for externally chosen algorithm variants; State is for internally triggered behavior changes.

Related Patterns

  • State Pattern (S12, Tutorial 4): Structurally identical to Strategy — both use composition and interface delegation. The difference is intent: Strategy lets the client choose an algorithm externally; State lets the object change its own behavior based on internal state transitions

  • Template Method Pattern (S12, Tutorial 5): Also defines algorithm variants, but uses inheritance instead of composition. Template Method defines the algorithm skeleton in a base class and lets subclasses override specific steps. Strategy extracts the entire algorithm into separate objects — offering more flexibility at the cost of more classes

  • Factory Method Pattern (S10, Tutorial 3): Often used together with Strategy. Factory Method encapsulates the decision of which strategy to create, while Strategy encapsulates the algorithm itself. "Factory decides which, Strategy encapsulates how"

  • Command Pattern (S12, Tutorial 3): Both encapsulate behavior as objects, but with different purposes. Command encapsulates a request (with parameters, undo support, queuing); Strategy encapsulates an algorithm (interchangeable processing logic)

Key Takeaways

  • The Strategy Pattern encapsulates interchangeable algorithms in separate classes behind a common interface, letting the context delegate work without knowing which algorithm runs

  • It directly implements the Open/Closed Principle (S6) — add new strategies without modifying existing code

  • Strategy replaces conditional branching (if/elif/else on algorithm type) with polymorphic dispatch — each branch becomes its own class with a focused responsibility

  • The pattern separates what varies (the algorithm) from what stays the same (the context's workflow), which is a fundamental object-oriented design principle

  • You'll see Strategy applied extensively in the Design Problems course — Parking Lot uses it for pricing flexibility, Elevator for scheduling algorithms, Uber for driver matching and surge pricing, Rate Limiter for throttling algorithms, and many more