Home › Low-Level Design › Strategy Pattern
S

Strategy Pattern

Strategy lets you swap an algorithm at runtime without touching the code that uses it. It is the pattern I sketch the instant a candidate's design starts drowning in if-else branches, and it is the natural answer to a whole family of LLD prompts. Learn it well and you can draw it, code it, and defend it in under two minutes.

Low-Level DesignEasy9 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Intent. Swap one of several interchangeable algorithms at runtime, behind a shared interface, without reopening the caller.
  2. Three roles. Strategy (the interface), Concrete Strategy (each implementation), Context (holds one strategy and delegates to it).
  3. Injection is the trick. The Context receives its strategy from outside (constructor or setter), it never picks its own. That is what makes it swappable and testable.
  4. Open-Closed in practice. A new variant is a new class, not an edit to code that already works and is tested.
  5. The classic gotcha. Moving the giant switch inside the Context defeats the whole pattern. You renamed the conditional, you did not delete it.

1The problem it solves#

Start with the pain, because that is what the pattern is reacting to. Picture a checkout service that calculates shipping cost. The first version is small and fine:

double shippingCost(Order order, String method) {
    if (method.equals("STANDARD")) {
        return order.weight() * 0.5;
    } else if (method.equals("EXPRESS")) {
        return order.weight() * 0.5 + 10;
    } else if (method.equals("OVERNIGHT")) {
        return order.weight() * 0.5 + 25;
    }
    throw new IllegalArgumentException("unknown method");
}

Then it grows, the way these always do.

  • A product manager asks for a fourth option. Then a fifth.
  • One new option calls an external carrier API. One depends on the day of the week.
  • Six months later the method is 180 lines and every branch has its own bug history.
  • Nobody wants to touch it, because changing one path risks breaking the other four.

The real problem is not the if-else, it is the ownership.

  • One class now knows how to do every version of the job.
  • It changes every time any single version changes.
  • That breaks the Open-Closed Principle (the O in SOLID): open for extension, closed for modification.
  • You keep editing a class that was already working and tested, instead of adding new code beside it.

Strategy fixes this by pulling each variant into its own class behind a shared interface.

The intent in one line

Define a family of algorithms, put each one in its own class, and make them interchangeable behind a common interface. The caller chooses which one to use and never learns how any of them work.

2The idea (mental model)#

The mental model: one task, many interchangeable ways to do it.

  • Define the shape of the task as an interface.
  • Each way of doing it becomes its own small class.
  • The object that needs the task done holds a reference to one of those classes and just calls it, never knowing which concrete one it got.

A phone camera makes it click.

  • The modes are "Portrait", "Night", "Panorama".
  • The shutter button does the same thing from your point of view: tap it, get a photo.
  • The algorithm behind the shutter is swapped by the mode you picked.
  • The button is the Context. The modes are the strategies. The button does not contain the night-mode math, it delegates.

This is composition over inheritance, live. Instead of a tall class tree with behavior baked in per subclass at compile time, you compose behavior by holding an object and swapping it. Three roles, that is it:

  • Strategy: the interface that declares the one operation.
  • Concrete Strategy: each implementation of that operation.
  • Context: the class that holds a strategy and delegates to it.

3Structure#

The Context owns a reference to a Strategy, and it never builds one itself.

  • You inject the strategy from outside, through the constructor or a setter.
  • That injection is the whole trick: it is what lets you switch behavior at runtime.
  • It is also what makes the Context testable: in a test you pass a fake strategy and assert the Context delegated to it.

Each Concrete Strategy implements the interface and nothing more.

  • They do not know about each other.
  • They often do not even know about the Context.
  • They just do their one job, which is textbook Single Responsibility.
classDiagram class ShippingStrategy { <> +cost(Order order) double } class StandardShipping { +cost(Order order) double } class ExpressShipping { +cost(Order order) double } class OvernightShipping { +cost(Order order) double } class Checkout { -ShippingStrategy strategy +setStrategy(ShippingStrategy s) void +total(Order order) double } ShippingStrategy <|-- StandardShipping ShippingStrategy <|-- ExpressShipping ShippingStrategy <|-- OvernightShipping Checkout o-- ShippingStrategy
Notice the relationship between Checkout and ShippingStrategy is aggregation (the open diamond), not composition. The strategy lives on its own and gets handed in, so Checkout does not create it or own its lifecycle. If Checkout built its own strategies internally, you would be back to the Context knowing all the variants and you would have solved nothing.

Here is the runtime flow when someone checks out with express shipping selected:

sequenceDiagram participant Client participant Checkout participant ExpressShipping Client->>Checkout: setStrategy(ExpressShipping) Client->>Checkout: total(order) Checkout->>ExpressShipping: cost(order) ExpressShipping-->>Checkout: 42.0 Checkout-->>Client: 42.0
The Client picks the strategy, the Checkout just delegates. Checkout has no idea express math is different from overnight math, and that ignorance is exactly the point.

4Implementation in Java#

interface ShippingStrategy {
    double cost(Order order);
}

class StandardShipping implements ShippingStrategy {
    public double cost(Order order) {
        return order.weight() * 0.5;
    }
}

class ExpressShipping implements ShippingStrategy {
    public double cost(Order order) {
        return order.weight() * 0.5 + 10;
    }
}

class OvernightShipping implements ShippingStrategy {
    public double cost(Order order) {
        return order.weight() * 0.5 + 25;
    }
}

class Checkout {
    private ShippingStrategy strategy;

    Checkout(ShippingStrategy strategy) {
        this.strategy = strategy;
    }

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

    double total(Order order) {
        return order.subtotal() + strategy.cost(order);
    }
}

Using it:

Checkout checkout = new Checkout(new StandardShipping());
double a = checkout.total(order);          // standard rate

checkout.setStrategy(new ExpressShipping());
double b = checkout.total(order);          // express rate, same object

What changed in Checkout when I needed a new shipping option? Nothing.

  • You write a new class that implements ShippingStrategy and pass it in.
  • The Checkout class never reopens.
  • That is Open-Closed in practice, not in theory.

Two things worth saying out loud to an interviewer.

  • Each strategy here is stateless, so you can reuse a single instance across every request (or make it a singleton). Nothing per-call to protect means it is thread-safe by default.
  • The interface has a single abstract method, which makes it a functional interface in Java. For simple cases you can skip the classes entirely:
ShippingStrategy flatRate = order -> 7.99;
checkout.setStrategy(flatRate);

A lambda is a strategy. That is not a trick, it is the same pattern with less ceremony. Mention it, because it shows you understand the pattern is about the shape of the behavior, not the class count.

5A real-world example#

Payment processing is the clearest production case.

  • A checkout flow supports cards, UPI, wallets, and bank transfer.
  • Each has completely different code: different SDKs, different validation, different failure handling, different retry rules.
  • Jam all of that into one processPayment with a switch, and every new method is surgery on critical money code, the code you least want to re-test under pressure.

Strategy makes each provider a class.

  • Define PaymentMethod with a pay(amount) method.
  • CardPayment, UpiPayment, WalletPayment each implement it.
  • The order service holds whichever one the user selected.
  • Add a new provider next quarter by adding one class and wiring it into the selection map. The order service does not change, which matters a lot when the thing you are afraid to break is the thing that takes money.

You have already been using Strategy in the standard library.

  • Comparator in Java is pure Strategy: Collections.sort(list, comparator) takes the comparison algorithm as a parameter, so sorting itself does not care how two items compare.
  • ThreadPoolExecutor takes a RejectedExecutionHandler, the strategy for "what do we do when the queue is full", and the JDK ships four: AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy.
  • Once you see the pattern, you see it everywhere.

6When to use it, and when not to#

Pause before the rule of thumb: you have exactly two cases, "member" and "guest", and they will never grow. Interface plus two classes, or a plain if-else?
Reveal the answer
Plain if-else. Strategy earns its keep when the set of variants grows and each is genuinely swappable behind one signature. Two fixed cases is complexity for its own sake, and an interviewer clocks the candidate who gold-plates a coin flip just as fast as the one who cannot stop writing if-else.

Reach for Strategy when:

  • You have several ways to do one thing and you pick between them at runtime or by configuration.
  • You keep adding new variants and dread editing the same method each time.
  • You want to test each variant in isolation, far easier when each is its own class with its own small test.
  • You have a big conditional that switches purely on a type or a mode.

Skip it when:

  • There are exactly two cases and they will never grow. A clean if-else is honestly fine.
  • The variants are not really interchangeable: they need different inputs or return different shapes. If the method signature cannot stay the same across all of them, Strategy is fighting you.
  • The behavior is fixed at compile time per subclass and never changes at runtime. That might just be ordinary inheritance.

The honest test: are these things genuinely swappable behind one signature, and do I expect the set to grow? Two yeses, use Strategy. Otherwise do not reach for it just because you can name it.

From the interviewer's chair

When I describe a system with "pluggable" or "configurable" behavior, I am fishing for this pattern. What I score is not whether you can recite the three roles, it is whether you reach for it at the right moment and refuse to reach for it at the wrong one. Naming when Strategy is overkill tells me more than naming when it fits.

7Common mistakes#

The red flag that fails you here

Building all the strategy classes, then writing if (type == CARD) strategy = new CardPayment() inside the Context. You moved the giant conditional, you did not delete it. The Context is supposed to receive a strategy, not decide which one. This is the single most common way candidates think they used Strategy and actually did not.

Letting the Context pick the strategy with a switch.

  • Push that selection out to a factory, or a map keyed by type, or let the caller inject the choice.
  • The Context should receive a strategy, not decide it.

Making strategies carry state they should not.

  • Keep concrete strategies as close to pure functions as you can.
  • A strategy that hoards mutable fields loses the ability to be shared safely across threads, and creates coupling where there was none.

A fat interface.

  • Strategy is at its best with a single method, or a very tight set.
  • If the interface grows to five methods and each concrete class implements two and stubs out the rest, that is the Interface Segregation Principle telling you the variants are not as interchangeable as you assumed.

Over-engineering trivial choices.

  • The flip side: do not pattern-match your way into three files to decide between two constants.

Confusing it with State (the one interviewers probe on purpose).

  • Strategy and State look identical on a class diagram. The difference is intent.
  • Strategy variants are chosen from outside and do not know about each other.
  • State objects swap themselves based on internal transitions.
  • Same skeleton, different story. Be ready to say that out loud.

Same class diagram, different intent: Strategy is chosen from outside, State switches itself from within.

8Where it shows up in interviews#

Strategy is one of the highest-value patterns to have loaded, because it answers a whole family of prompts.

  • Payment system: different payment methods behind one interface. Almost guaranteed to appear.
  • Discount or pricing engine: percentage off, flat off, buy-one-get-one, each a strategy.
  • Notification service: email, SMS, push, each a NotificationStrategy.
  • Parking lot or ride-share fare calculation: different pricing rules per vehicle type or time of day.
  • Sorting, routing, matchmaking: anywhere the "how" of a step is interchangeable.

The move that scores points: when you hit a conditional on type during a design, pause and say, "I will pull each of these into a strategy behind a common interface, so adding a new one does not touch this class." Then draw the three boxes and the aggregation arrow. That one sentence signals you think about change, not just the happy path. If they ask how you would pick the strategy at runtime, mention a factory or a registry map keyed by type, so the selection logic also lives in one swappable place.

Keep the vocabulary crisp: Strategy, Concrete Strategy, Context, injection, Open-Closed. Say those words, draw the aggregation arrow, and you have demonstrated the pattern cleanly in under two minutes.

Follow-ups the interviewer will ask (and your answers)
"How is this different from the State pattern? They look identical."
Same class diagram, different intent. Strategy variants are picked from outside the Context and do not know about each other. State objects swap themselves based on internal transitions, so each state often knows the next state. If the object is choosing its own next behavior, it is State. If something external is choosing for it, it is Strategy.
"Where does the Context get the right strategy from, then?"
Not from a switch inside the Context, that defeats the pattern. The caller injects it, or a factory or a registry map keyed by type resolves it once and hands it in. The selection logic lives in one swappable place, and the Context stays ignorant of the variants.
"Do I need a class per variant? That feels heavy for simple cases."
No. When the interface has a single method it is a functional interface, so a lambda is a valid strategy: `ShippingStrategy flat = order -> 7.99;`. Same pattern, less ceremony. Reach for full classes when a variant has real logic, dependencies, or its own tests.
"Are these strategies thread-safe? Can I share one instance?"
If they are stateless, yes, share a single instance or make it a singleton. There is nothing per-call to protect. The common bug is a strategy that hoards mutable fields, which quietly makes it unsafe to share and couples it to call order.
"When would you not use Strategy here?"
When there are two fixed cases that will never grow (a clean if-else wins), when the variants need different inputs or return different shapes so one signature cannot hold them, or when the behavior is fixed at compile time per subclass, which is just inheritance.

What you can now explain in the room

  • The intent of Strategy in one line, and the three roles: Strategy, Concrete Strategy, Context.
  • Why injecting the strategy (not building it inside the Context) is what makes the pattern work and makes it testable.
  • How Strategy satisfies Open-Closed: a new variant is a new class, not an edit to tested code.
  • The classic misuse: hiding a switch inside the Context, which renames the conditional instead of removing it.
  • When to skip Strategy, and why over-engineering a two-case choice is also a red flag.
  • How Strategy differs from State on the same class diagram, by intent rather than shape.

Next, look at the Factory pattern (how you choose which strategy to hand in) and the State pattern (Strategy's near-twin), plus LLD problems like the payment gateway and the discount engine where this shows up front and center.

Get a prep plan built around your Strategy Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Strategy Pattern prep plan is on the way.