Free Interactive Course · Design Patterns

Strategy Design Pattern

Swap the algorithm at runtime, and delete the conditional that used to choose it.

Behavioural Patternseasya.k.a. Policy
ShareXLinkedIn
In one sentence

Define a family of interchangeable algorithms, encapsulate each one, and make them substitutable without changing the code that uses them.

01

The problem Strategy solves

The problem

Checkout applies a discount. At first there is one rule, so total() has an if. Then marketing adds a percentage-off code, then buy-one-get-one, then loyalty tiers, then a Black Friday rule that expires in four days.

total() is now a hundred-line conditional. It changes for five unrelated business reasons, every change risks the other four, and the tests either mock the clock or run four seconds. Nobody can add a rule without reading all of it.

The clue that this is the wrong shape: the branches don't share anything. They aren't variations on one calculation, they're different calculations that happen to live in the same method.

02

How the Strategy pattern works

Pull each branch into its own object behind a shared interface, then pass in the one you want:

  1. Define a strategy interface with one method — apply(cart) → Money.
  2. Each former branch becomes a concrete strategy: small, independently testable, with no knowledge of the others.
  3. The context (Checkout) holds a strategy and calls it. The conditional is gone, because choosing now happens once at the edge of the system rather than every time the total is computed.
Strategy vs. State — the question everyone gets asked. The structure is nearly identical: a context delegating to an interchangeable object. The difference is who decides and why. With Strategy, the client picks the algorithm and it doesn't change on its own — a percentage discount doesn't spontaneously become BOGO. With State, the objects replace themselves as a result of what happens: Pending becomes Paid becomes Shipped. Strategy is a choice; State is a lifecycle.
Checkout- policy: DiscountPolicy+ total()«interface» DiscountPolicy+ apply(cart) : MoneyPercentageOffBuyOneGetOneLoyaltyTierthe hundred-line conditional became three small classes and one interface
Participants. The Context holds a strategy and delegates. The Strategy interface is usually a single method. Concrete Strategies are the old if branches, each now independently testable. The context has no branch left in it at all.
03

See it: swap the algorithm, don't touch the caller

Apply each policy, then add a brand-new one. Checkout.total() calls the same single line every time and never needed editing.

▶ Try it — swap the policy

An interactive dispatcher: selecting a discount policy changes which concrete strategy handles the calculation, and adding a brand-new policy requires no change to Checkout.total().

04

Strategy pattern code examples

Interchangeable discount policies behind one interface.

@FunctionalInterface
public interface DiscountPolicy {
    Money apply(Cart cart);
}

public final class PercentageOff implements DiscountPolicy {
    private final BigDecimal rate;
    public PercentageOff(BigDecimal rate) { this.rate = rate; }

    @Override public Money apply(Cart cart) {
        return cart.subtotal().times(BigDecimal.ONE.subtract(rate));
    }
}

public final class BuyOneGetOne implements DiscountPolicy {
    @Override public Money apply(Cart cart) {
        return cart.subtotal().minus(cart.cheapestOfEachPair());
    }
}

public class Checkout {
    private final DiscountPolicy policy;   // injected, not chosen here
    public Checkout(DiscountPolicy policy) { this.policy = policy; }

    public Money total() {
        return policy.apply(cart).plus(shipping()).plus(tax());
    }
}

// Because the interface is @FunctionalInterface, a whole strategy can be a
// lambda when it doesn't need a name or state:
//
//     new Checkout(cart -> cart.subtotal());          // NoDiscount
//     new Checkout(new PercentageOff(new BigDecimal("0.10")));
Read across the tabs and the pattern almost disappears. In JavaScript, Python and TypeScript a strategy is a function and a "parameterised concrete strategy" is a closure — the class hierarchy that GoF drew was working around languages that had no way to pass behaviour around. Go keeps the interface because interfaces there are free. C++ is the interesting outlier: the template form resolves the choice at compile time, which is why std::sort with a custom comparator costs nothing at runtime.
05

How to implement Strategy

  1. Find the conditional whose branches don't share code. That's the signal — variations on one algorithm belong in Template Method; genuinely different algorithms belong here.
  2. Define the narrowest possible interface. One method is ideal, because then a lambda or a plain function can be a strategy.
  3. Move each branch into its own type, passing configuration through its constructor rather than through the method.
  4. Have the context receive the strategy from outside — constructor, parameter, or DI. A context that picks its own strategy has just moved the conditional.
  5. Push selection to one edge of the system: a lookup table keyed by promo code, a config value, a route. One place to read, one place to extend.
  6. Give the "nothing happens" case a real strategy (NoDiscount) instead of a null check. Null strategies are how the conditional sneaks back in.
06

When to use Strategy — and when not to

Use it when a class has several ways of doing one thing and the choice is made outside it; when a conditional keeps growing for unrelated business reasons; when you want to test each branch in isolation; or when a variation must be selectable at runtime, from config, or per tenant.

Where it goes wrong

Two branches that will never become three. An interface, two classes and a wiring decision to replace an if/else is a net loss in readability. Wait for the third.

The interface leaks the implementations. If apply(cart, isBlackFriday, loyaltyTier) grew parameters that only some strategies use, the abstraction is wrong — the context is still making decisions on their behalf. Pass configuration into the strategy's constructor instead.

A class where a function would do. In Python, JavaScript, TypeScript, C# and modern Java, a one-method strategy can be a lambda. Writing five classes with one method each is GoF cosplay.

Confusing it with State. If your strategies swap themselves based on what happened, you've built a State machine and should name it that — otherwise the self-replacement looks like a bug to the next reader.

07

Quick check

🧠 Quick check
Which of these is a State machine rather than a Strategy?

In the wild

JavaCollections.sort(list, comparator) — the comparator is the strategy, and Comparator being a functional interface is why a lambda works there.
Pythonsorted(items, key=...) and json.dumps(obj, default=...) — the callable you pass in IS the strategy.
C#IComparer<T>, and every Func<T, TResult> parameter in LINQ — Where(predicate) is Strategy on every call.
Gosort.Slice(s, less), and http.Handler — swapping the handler swaps the whole request-handling algorithm.
C++std::sort(begin, end, comp) — a compile-time strategy, which is why a custom comparator costs nothing at runtime.
JavaScriptArray.prototype.sort(compareFn) and every callback-taking method — passing behaviour is so normal in JS that the pattern is invisible.

Frequently asked questions

What is the difference between the Strategy and State patterns?
The class diagrams are nearly the same; the intent isn't. In Strategy the client chooses the implementation, and it stays chosen for the duration of the operation — a percentage discount never spontaneously becomes buy-one-get-one. In State the objects swap themselves as events occur, and each state typically knows which state follows it. Strategy models a choice; State models a lifecycle.
Is Strategy the same as passing a lambda?
In any language with first-class functions, yes — that is the pattern, minus the ceremony. sorted(items, key=...) in Python and list.sort(comparator) in Java are textbook Strategy. Keep the explicit interface and classes when a strategy needs configuration and a name worth seeing in a stack trace, when it grows a second method, or when you register implementations with a DI container.
When is Strategy overkill?
When there are two branches that will never become three, when the branches share most of their code (that's Template Method, not Strategy), or when the choice is genuinely fixed at compile time and will never come from configuration. Replacing a readable four-line if with an interface, two classes and a wiring decision makes the code longer and no more flexible.
How does Strategy relate to dependency injection?
DI is usually how the strategy arrives. The context declares that it needs an IDiscountPolicy, and the container supplies whichever concrete one is registered — that's the "receive the strategy from outside" step, automated. The important discipline is unchanged: the context must never choose its own strategy, or you've simply relocated the conditional.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.