Early Access: 87 spots left.

Claim
Low Level DesignStrategyStrategy: Refactor the Checkout Discount Engine

Course content

Strategy: Refactor the Checkout Discount Engine

Medium·Tagsdesign-patternsstrategycompositionrefactoring

Problem Statement

The starter `CheckoutService` accepts a `DiscountStrategy` in its constructor but does not actually delegate to it. Instead, `calculateTotal` does an `instanceof` chain on the strategy reference and computes the discount inline with hardcoded values: ```java public double calculateTotal(List<Double> itemPrices) { double subtotal = sum(itemPrices); if (strategy instanceof PercentageDiscount) { return subtotal * 0.9; // 10% off, hardcoded } else if (strategy instanceof FlatDiscount) { return Math.max(0, subtotal - 50); // flat $50, hardcoded } else if (strategy instanceof BogoDiscount) { ... // BOGO logic inline } else { return subtotal; } } ``` This is the wrong way to use a strategy. The reference is being treated as a tag ("which branch do I take?") instead of as a delegate ("compute the discount for me"). It also makes the strategy parameters useless: `new PercentageDiscount(20)` produces the same result as `new PercentageDiscount(10)` because the hardcoded `0.9` ignores the field on the strategy. Refactor so that the discount math lives in each strategy class and `CheckoutService.calculateTotal` becomes a one-line delegation. After your refactor, the design must expose this exact public API: ```java interface DiscountStrategy { double computeDiscount(List<Double> itemPrices); } class NoDiscount implements DiscountStrategy { ... } class PercentageDiscount implements DiscountStrategy { public PercentageDiscount(double percent); } class FlatDiscount implements DiscountStrategy { public FlatDiscount(double amount); } class BogoDiscount implements DiscountStrategy { ... } class CheckoutService { public CheckoutService(DiscountStrategy strategy); public void setDiscountStrategy(DiscountStrategy strategy); public double calculateTotal(List<Double> itemPrices); // delegates to strategy.computeDiscount(itemPrices); contains no instanceof. } ``` The four `computeDiscount` methods on the strategy classes are stubbed in the starter (they all return 0). Move the discount math out of `CheckoutService` and into the right strategy class. Then replace the body of `calculateTotal` with subtotal minus `strategy.computeDiscount(itemPrices)`. The validator runs ten checks that defeat the obvious cheats: 1. `new PercentageDiscount(10).computeDiscount([100, 200])` returns 30.0 when called directly. The percent logic must live in the strategy. 2. `new FlatDiscount(50).computeDiscount([100, 200])` returns 50.0. 3. `new FlatDiscount(500).computeDiscount([100, 200])` returns 300.0. Flat discount cannot exceed subtotal. 4. `new BogoDiscount().computeDiscount([10, 20, 30])` returns 20.0. Sort descending; items at odd indices (1, 3, ...) are free. 5. `new NoDiscount().computeDiscount([100, 200])` returns 0.0. 6. `new CheckoutService(new PercentageDiscount(20)).calculateTotal([100, 200])` returns 240.0. The validator deliberately uses 20% to defeat starter code that hardcodes 10%. 7. `setDiscountStrategy` actually swaps the strategy at runtime: same instance produces a different total after the swap. 8. The validator defines its own `DiscountStrategy` subclass (a 99%-off loyalty discount), injects it, and asserts the service uses it. Any `instanceof` chain on the strategy field fails this check. 9. The `CheckoutService` field that holds the strategy is declared as `DiscountStrategy` (the interface), not as a concrete class. 10. `CheckoutService` does not declare its own discount-computation methods (`applyPercentageDiscount`, `applyFlatDiscount`, `applyBogoDiscount`, etc.). The discount logic must live on the strategy classes. The smelly starter passes only the constructor and reflection shape; it fails every behavioural check until the refactor is real. Starter code and validators are available in Java, Python, and C++.

Examples

Example 1
Input
new PercentageDiscount(15).computeDiscount(List.of(200.0))
Output
"30.0"
Why
15% of 200 is 30. The strategy class owns the percent rule.
Example 2
Input
new CheckoutService(new BogoDiscount()).calculateTotal(List.of(10.0, 20.0, 30.0))
Output
"40.0"
Why
BOGO sorts descending [30, 20, 10] and frees items at odd indices: 20 is free. Total = 60 - 20 = 40.
Example 3
Input
service.setDiscountStrategy(new LoyaltyDiscount()); service.calculateTotal(prices);
Output
"depends on LoyaltyDiscount.computeDiscount"
Why
Adding a new discount type is a new class implementing DiscountStrategy. CheckoutService handles it without any change because it only depends on the interface.

Constraints

  • Refactor must keep these classes named: DiscountStrategy, NoDiscount, PercentageDiscount, FlatDiscount, BogoDiscount, CheckoutService.
  • DiscountStrategy must remain an interface with method `double computeDiscount(List<Double> itemPrices)`.
  • CheckoutService must accept a DiscountStrategy in its constructor and expose setDiscountStrategy(DiscountStrategy).
  • CheckoutService.calculateTotal must take only List<Double> and must not branch on the runtime type of its strategy field.
  • The strategy field on CheckoutService must be declared as DiscountStrategy (the interface), not a concrete class.

Hints

Stuck? Reveal a nudge toward the right pattern, one step at a time.

Hint 1
The discount math for each strategy belongs inside that strategy's computeDiscount method. PercentageDiscount.computeDiscount should sum the prices and return percent% of the subtotal. FlatDiscount.computeDiscount should return min(amount, subtotal). BogoDiscount.computeDiscount should sort the prices descending and sum the items at odd indices.
Hint 2
Once each strategy returns the correct discount, CheckoutService.calculateTotal becomes two statements: compute the subtotal, then return subtotal - strategy.computeDiscount(itemPrices).
Hint 3
Do not branch on the runtime type of the strategy field. The whole point of Strategy is that the using class never asks 'which kind of strategy do I have?'. It just calls computeDiscount and trusts the strategy to do the right thing.
Hint 4
If you keep the instanceof chain and just add new branches, you are not applying the pattern. The validator defines its own strategy class (LoyaltyDiscount) that you cannot anticipate, so any chain that lists known concrete classes will fall through and fail.
Hint 5
Watch out for the strategy field's declared type. If you write `private PercentageDiscount strategy;` instead of `private DiscountStrategy strategy;`, the constructor will reject anything other than PercentageDiscount and the substitutability is gone. Hold the field at the interface type.