Intent

Define a family of algorithms, encapsulate each one in its own object, and make them interchangeable so the code that uses them never has to change when you add another option.

The Problem

Have you ever opened a checkout service that started simple and turned into a 200-line if/elseif ladder? You shipped Stripe first. Then PayPal. Then bank transfers. Then Apple Pay. Each one squeezed in another branch.

PHP
public function charge(Order $order): Receipt
{
    if ($order->method === 'stripe') {
        // ... validate, hit Stripe API, log, return
    } elseif ($order->method === 'paypal') {
        // ... validate, hit PayPal API, log, return
    } elseif ($order->method === 'bank') {
        // ... generate IBAN slip, queue email, return
    }
    throw new InvalidPaymentMethod();
}

The class now knows about every payment method. Every test imports them all. Adding the next one means reopening code that already shipped — and if you regress, every checkout breaks. That's the smell Strategy was built to remove.

The Solution

Strategy says: pull each algorithm into its own object behind a tiny interface. The class that uses them stops caring which one is wired in.

PHP
interface PaymentStrategy
{
    public function charge(Order $order): Receipt;
}

CheckoutService keeps a PaymentStrategy reference and calls charge() — no branching. Want a new method? Write a class. The checkout never opens.

Real-World Analogy

Think of getting to the airport. You can take a taxi, a train, or a bike — different mechanics, different price, different timing — but the interface is identical: "get me to the airport." You pick a mode in the morning; the rest of your day doesn't care how it happened.

That's Strategy in a sentence. Same job, swappable engine, caller untouched.

Structure

Strategy pattern: a Context holds a reference to a Strategy interface; concrete strategies implement that interface.
Strategy: Context delegates the algorithm to a swappable Strategy.

Three roles you'll see in every Strategy implementation:

  • Context — the class clients call. Holds a reference to one strategy. In our example: CheckoutService.
  • Strategy — the interface every algorithm agrees on. Here: PaymentStrategy.
  • Concrete Strategy — one implementation per algorithm: StripeStrategy, PayPalStrategy, BankStrategy.

The arrow from Context → Strategy is the only contract. That's why you can add a fourth strategy without touching CheckoutService or any existing strategy.

Code Examples

Here's the same CheckoutService in five languages. Notice how the Context shrinks to delegation — the conditional is gone, and the shape of the solution is identical regardless of whether your language calls it an interface, an abstract class, or a struct that satisfies one.

interface PaymentStrategy {
  charge(order: Order): Receipt;
}

class StripeStrategy implements PaymentStrategy {
  charge(order: Order): Receipt {
    // Hit Stripe API, return Receipt.
  }
}

class PayPalStrategy implements PaymentStrategy {
  charge(order: Order): Receipt {
    // Hit PayPal API, return Receipt.
  }
}

export class CheckoutService {
  constructor(private payment: PaymentStrategy) {}

  settle(order: Order): Receipt {
    return this.payment.charge(order);
  }
}
from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def charge(self, order): ...

class StripeStrategy(PaymentStrategy):
    def charge(self, order):
        # Hit Stripe API, return Receipt.
        ...

class PayPalStrategy(PaymentStrategy):
    def charge(self, order):
        # Hit PayPal API, return Receipt.
        ...

class CheckoutService:
    def __init__(self, payment: PaymentStrategy):
        self._payment = payment

    def settle(self, order):
        return self._payment.charge(order)
public interface PaymentStrategy {
    Receipt charge(Order order);
}

public final class StripeStrategy implements PaymentStrategy {
    @Override
    public Receipt charge(Order order) {
        // Hit Stripe API, return Receipt.
        return new Receipt();
    }
}

public final class PayPalStrategy implements PaymentStrategy {
    @Override
    public Receipt charge(Order order) {
        // Hit PayPal API, return Receipt.
        return new Receipt();
    }
}

public final class CheckoutService {
    private final PaymentStrategy payment;

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

    public Receipt settle(Order order) {
        return payment.charge(order);
    }
}
<?php

namespace App\Services;

interface PaymentStrategy
{
    public function charge(Order $order): Receipt;
}

final class StripeStrategy implements PaymentStrategy
{
    public function charge(Order $order): Receipt
    {
        // Hit Stripe API, return Receipt.
    }
}

final class PayPalStrategy implements PaymentStrategy
{
    public function charge(Order $order): Receipt
    {
        // Hit PayPal API, return Receipt.
    }
}

final class CheckoutService
{
    public function __construct(private PaymentStrategy $payment) {}

    public function settle(Order $order): Receipt
    {
        return $this->payment->charge($order);
    }
}
package checkout

type PaymentStrategy interface {
    Charge(order Order) Receipt
}

type StripeStrategy struct{}

func (s StripeStrategy) Charge(order Order) Receipt {
    // Hit Stripe API, return Receipt.
    return Receipt{}
}

type PayPalStrategy struct{}

func (p PayPalStrategy) Charge(order Order) Receipt {
    // Hit PayPal API, return Receipt.
    return Receipt{}
}

type CheckoutService struct {
    payment PaymentStrategy
}

func NewCheckoutService(payment PaymentStrategy) *CheckoutService {
    return &CheckoutService{payment: payment}
}

func (c *CheckoutService) Settle(order Order) Receipt {
    return c.payment.Charge(order)
}

The Context's body is one line in every language — and in Go, where interfaces are implicit, there isn't even an implements keyword to write. That's the whole point: when the algorithm gets swapped, the calling code doesn't move.

When to Use It

Reach for Strategy when you can answer "yes" to any of these:

  • You have multiple variants of the same operation. Sorting, pricing, validation, payment, shipping cost — anywhere you'd otherwise write if (type === ...).
  • You want new variants without touching the caller. Strategy is the Open/Closed Principle in its simplest form.
  • You want to test variants independently. Each ConcreteStrategy is its own unit test, and the Context test mocks the interface.
  • The algorithm needs to change at runtime. Users pick currencies; admins toggle pricing rules. Re-assigning the strategy reference is enough.

If a behavior is fixed at compile time and you're sure it'll never grow another branch, don't reach for Strategy. Two if lines aren't a smell — five usually are.

Pros and Cons

Pros

  • New algorithms drop in without changing existing ones — open for extension, closed for modification.
  • Each strategy is small, focused, and easy to test in isolation.
  • The Context stops knowing about implementation details.
  • Strategies can be picked at runtime — by config, by request, by user role.

Cons

  • More files. If you only ever have two algorithms, the abstraction earns less than it costs.
  • Clients of the Context need a way to pick a strategy. That picker has to live somewhere — usually a Factory.
  • Strategies can drift if their interface stays too narrow. Keep the contract honest.

Pro Tips

  • Keep the interface tight. One method is ideal; two is fine. Three is a sign you're hiding two responsibilities behind one name.
  • Inject the strategy. Construct it in the container, factory, or controller — never new it inside the Context.
  • Name strategies by what they do, not what they are. StripeStrategy works, but CardPaymentStrategy survives the day Stripe gets replaced.
  • Resist the urge to share state between strategies. If two strategies need the same helper, lift it out — don't make them inherit each other.

Relations with Other Patterns

  • State looks identical on a UML diagram. The difference is intent: Strategy lets the client swap behavior; State lets the Context swap its own behavior as it transitions.
  • Command is Strategy with extra payload — a Command bundles the algorithm plus the arguments to call it with.
  • Factory Method is often the picker that gives you the right Strategy for a given input.
  • Decorator wraps an existing object to add behavior; Strategy replaces it. If you find yourself wrapping a strategy in a strategy, Decorator is what you want.

Final Tips

The first time I refactored a switch ladder into Strategy I thought I was over-engineering. Six months later, our team added a fourth payment method on a Friday afternoon — one new class, no edits to existing code, no regressions. Nobody on call paged us that weekend. That's the moment Strategy earned its keep.

Use it when the branches keep coming. If they don't, leave the if. Good luck with the next refactor — the smell will tell you when it's time.