Early Access: 87 spots left.

Claim

Course content

No content available

A checkout service computes an order total. The subtotal math never changes, but the discount rule changes all the time: ten percent off during a sale week, a flat amount off with a coupon, buy one get one free on the weekend. The Strategy pattern takes the step that varies, pulls each variant out into its own small class behind a shared interface, and has the checkout hold a reference to one of them and call it through the interface.

One at a time is the point. The checkout owns a single DiscountStrategy reference; it does not know which promotion is behind it, and it never runs more than one. Swapping behaviour means handing the checkout a different object, not editing its code. The vocabulary is small and worth using precisely, because interviewers expect it: the class doing the work is the context, the shared interface is the strategy, and each variant class is a concrete strategy.

The rest of this page builds exactly that checkout. First, the version the pattern rescues you from, with every discount rule inlined into the service and selected by a string.

CheckoutService is doing two unrelated jobs. The first is orchestration: walk the cart, compute a subtotal, return a number. The second is discount mathematics, and it has four different versions stuffed into one method. Whenever marketing introduces a new promotion (loyalty tiers, weekend specials, BNPL combos), the only sensible-looking edit is to add another branch.

This is also painful to test. To exercise the BOGO logic you have to construct a CheckoutService, build a list of prices, pass the literal string "BOGO", and then back-derive whether the discount math is right from the final total. The actual algorithm has no surface of its own; it is buried inside a conditional in someone else's class. And every time the business changes one promotion's rule, you re-test all of CheckoutService because they all share the same method.

What this costs in production

A Black Friday BOGO promotion shipped as one more else if inside calculateTotal. The new branch had an off-by-one in its loop, and because every promotion shares that single method, the bad build went out alongside the percent-off and flat-off rules that nobody had touched. QA had signed those off weeks earlier and never thought to re-run them. For the first three hours of the sale, carts with an odd number of items were overcharged before anyone traced it back to the deploy.

The checkout is one instance of a shape that repeats all over production code: exporters branching on format, notifiers branching on channel, shipping calculators branching on carrier. Nobody announces that a strategy is needed; you spot it from symptoms like these.

SmellWhat it looks likeWhy a strategy is the answer
An if/else or switch chain on a configuration string inside an otherwise stable classif (mode.equals("PERCENT_OFF")) { ... } else if (mode.equals("FLAT_OFF")) { ... }Each branch is a separate algorithm pretending to share a method body
A class that grows every time the business adds a new variantCheckoutService gains a new branch with every promotion typeThe variants do not belong to CheckoutService; they belong to their own classes
A method that takes a discriminator parameter and immediately branches on itcalculateTotal(items, discountType) starts with a switch on discountTypeThe discriminator is doing the job that polymorphism should be doing
Different feature flags or experiments need different behaviour at the same call siteif (experimentEnabled) { newAlgorithm(); } else { oldAlgorithm(); }The two algorithms are strategies; the call site should hold one or the other, not both
Tests have to set up the entire class to test one branch of the chainTo test the BOGO discount you instantiate CheckoutService with full carts and assert end-to-endThe algorithm should be testable in isolation as a pure object

Each promotion is now a small object with one job: take a list of prices, return a discount amount. They are independent of CheckoutService and of each other, which means you can unit-test BogoDiscount directly against a price list without touching anything else.

CheckoutService itself is now four lines of orchestration: sum the prices, ask the strategy for a discount, subtract, return. It does not know which promotion it is running and it does not need to. When the business adds a tiered loyalty discount next month, you write a LoyaltyDiscount class and pass it into the service. No existing file changes. No regression risk on the other promotions.

The other thing the refactor unlocks is runtime swapping. setDiscountStrategy is a one-liner because the field is just a reference to the interface. The same CheckoutService instance can run a percent-off promotion in the morning and a BOGO promotion in the afternoon. The same hook is what lets you wire feature flags or A/B tests at the strategy level: the experiment framework decides which DiscountStrategy to install, the rest of the code is untouched.

One objection comes up every time someone meets this refactor: the branching on the discount code has not disappeared, it has only moved. That is true, and it is the point. Selection and execution used to be tangled inside one method that every promotion shared, so touching either meant touching both. Now execution is a polymorphic call on whichever strategy is installed, and selection lives in one boring factory at the edge of the system, the only place in the codebase that knows all the variants exist.

This is also where a question beginners rightly ask gets a definite answer: do all the strategies run? No. The factory picks one, the constructor installs it, and every subsequent calculateTotal call runs that single object until someone swaps it. Nothing ever loops over the strategies. If your design wants several of these objects to act on the same request, you have drifted into a different pattern, a chain of handlers or an event with observers, and the comparison further down this page draws those lines.

delegates toimplementsCheckoutServiceDiscountStrategyNoDiscountPercentageDiscountFlatDiscountBogoDiscount
Strategy structure: the context holds a reference to the DiscountStrategy interface and delegates the discount math to it. Each promotion is an interchangeable concrete strategy, so adding one never touches the context.

With the refactor in hand, it is worth pinning down what Strategy is not, because several neighbouring ideas share its silhouette.

Strategy vs. State: both pull behaviour into a separate object. The difference is intent. Strategy variants are independent and the choice is usually made by the caller ("use the percent-off discount today"). State variants represent a lifecycle and the object transitions between them based on internal events (Order.confirm() moves you from Pending to Confirmed). If the variants know about each other and one decides what comes next, you are looking at State, not Strategy.

Strategy vs. an OCP-style polymorphic refactor: an OCP refactor typically makes the data type itself polymorphic. The thing being processed is the abstraction (Payment with subclasses). Strategy keeps the data plain and makes the algorithm the abstraction. A Cart with a list of items is just data; the discount rule is a separate object plugged into the checkout service. Use Strategy when the same data needs to be processed in different ways depending on context, configuration, or experiment, and the data itself has no business knowing which way.

Strategy vs. Observer vs. Chain of Responsibility: all three hang several implementations off one interface, which is exactly why they blur together when you are new to them. The difference is head count at the call site. A Strategy context holds exactly one implementation and calls it. An Observer subject holds a list of listeners and notifies all of them. A chain holds an ordered sequence of handlers and passes the request along until one accepts it. When you sketch the structure in an interview, say the cardinality out loud ("the checkout holds exactly one DiscountStrategy"); it is a cheap sentence that tells the interviewer you know more than the shape of the diagram.

Strategy is one of the patterns you have probably already used without naming it, because sorting is the canonical strategy in every mainstream language. Java's Collections.sort(list, comparator) is the context; the Comparator you pass in is the strategy, and two comparators sort the same list two different ways without the sort algorithm changing. Python's sorted(names, key=len) is the same pattern with a plain function standing in for the strategy object. C++'s std::sort(v.begin(), v.end(), byPriceDesc) accepts any callable as the comparison, from a function pointer to a lambda to a hand-written functor class. Three syntaxes, one idea: the container walk stays fixed, and the rule that varies arrives as an argument.

Standard libraries are full of micro-strategies. In Java, any method that accepts a Function, Predicate, Consumer or Supplier is asking the caller to hand over an algorithm, and stream.filter(predicate).map(function) is a pipeline of them. Python leans on first-class functions so hard that the pattern becomes nearly invisible: re.sub can take a function that computes each replacement, json.dumps(default=...) takes the fallback serialiser, and collections.defaultdict(list) takes the factory to call on a missing key. C++ hides strategies in template parameters: std::unordered_map takes its hash and equality as functor types, std::priority_queue takes its comparison, and std::unique_ptr takes a custom deleter that runs at destruction.

Frameworks use the pattern at architecture scale, and password hashing is the same worked example in two ecosystems. Spring Security's PasswordEncoder interface has BCryptPasswordEncoder, Argon2PasswordEncoder and NoOpPasswordEncoder (for tests) behind it; configuration installs one, and the login code never learns which hash it is using. Django does the identical thing through its PASSWORD_HASHERS setting, which picks among hasher classes the same way. Back in the JDK, ThreadPoolExecutor takes a RejectedExecutionHandler (AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy) as the strategy for what to do when the queue is full.

The pattern even explains a little language history. Java 8 lambdas are syntactic sugar for one-method strategy classes, which is exactly why list.sort((a, b) -> a.length() - b.length()) slots in anywhere a Comparator is wanted. Python never needed the ceremony, since functions are already objects, so many textbook Strategy examples collapse to "pass a function". C++ lets you choose when the strategy is bound: a virtual interface or std::function swaps at runtime like the checkout example, while a template parameter fixes the strategy at compile time with zero call overhead, which is the trick the STL's allocators are built on. In all three languages the explicit strategy class still earns its keep the moment the algorithm carries configuration or deserves tests of its own.

ProsCons
Each algorithm is independently testable as a small objectAdds an interface and N classes for what was a switch
New variants are purely additive; the context never changesThe context needs the strategy from somewhere; configuration burden moves to the caller
Runtime swappability supports feature flags and A/B tests at the algorithm levelInline simplicity is lost for cases where one algorithm is plenty
Clean separation between orchestration (the context) and algorithm (the strategy)Strategy interface becomes a god-interface if variants need different inputs; choosing the right method signature is non-trivial
Lambdas and first-class functions make single-method strategies almost free in modern languagesStack traces and code navigation get one extra hop through the strategy interface

Strategy pays off when there are genuinely interchangeable algorithms and the choice is going to vary. If there is exactly one algorithm and there has never been talk of a second, an interface and one implementation are pure ceremony. A plain method on the class that needs it is fine until a second variant actually shows up.

The other failure mode is creating a strategy interface so generic that every strategy needs a different shape of input or output. If PercentageDiscount needs only the subtotal but LoyaltyDiscount needs the user's purchase history, you end up either passing a god-object context to every strategy or splitting the interface and losing the substitutability that is the whole point of the pattern. The fix is usually to narrow the interface to what every variant actually needs, and have each strategy fetch its own extra data from collaborators it asks for in its constructor.

Three flavours of question recur. The first is design: "design a payment system that supports multiple payment methods" or "design a pricing engine for a marketplace". The expected first move is to name the dimension that varies (payment method, pricing rule) and let the interviewer see you sketch the strategy interface and the context. The follow-up tends to be about adding a new variant; that is your cue to point out that adding a new strategy is one new file and the rest is untouched.

The second is comparison: "Strategy vs. State", "Strategy vs. Template Method", "Strategy vs. plain polymorphism". The State, refactor, Observer and Chain contrasts are drawn earlier on this page; rehearse them until each is one sentence. The one name not covered yet is Template Method: it keeps the varying step inside the class family, as a method that subclasses override, while Strategy moves the varying step into a separate object the context holds. That trade is composition over inheritance, and it is the phrase interviewers are listening for.

The third is implementation choice: "would you use a class or a lambda?". The honest answer is "it depends". Lambdas, plain Python functions, and C++ callables are cleaner for stateless one-method strategies (a comparator, a key function, a predicate). Classes are clearer when the strategy holds configuration (PercentageDiscount(20)), needs unit tests of its own, or is going to be referenced by name (logging, telemetry, config injection). Saying you would always pick one or the other is the wrong answer; saying when each fits is the right one.

The companion exercise gives you a CheckoutService with a switch on a discount string and asks you to refactor it into a DiscountStrategy interface with four concrete strategies. The validator checks each strategy in isolation, that CheckoutService accepts a DiscountStrategy via its constructor, that the strategy can be swapped at runtime, and that an entirely new strategy defined inside the validator works without touching CheckoutService.

Back to Low Level Design