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.

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.