Shopping Cart
Everyone's added an item to a cart, applied a coupon, and watched the total drop. The interview question underneath that mundane flow is: how many discounts can stack, in what order, and how do you add a new one without editing every discount that came before it.
Requirements
Functional
- A customer adds and removes
Products from aCart, optionally changing quantity. - The cart computes a subtotal from its line items.
- Multiple discounts can apply to the same cart - a percentage-off coupon, a flat-amount voucher, a free-shipping threshold - and they need to combine predictably, not just overwrite each other.
- Checkout produces a final total after every applicable discount has run.
Non-functional
- Adding a new kind of discount must not require editing
Cartor any discount that already exists - only writing the new one and hooking it into the chain. - The order discounts apply in must be explicit and inspectable, not implicit in whichever
order
ifstatements happen to be written.
Design
Each PriceRule is a link in a chain: it looks at the cart, decides whether it applies, does
its own adjustment, and passes the running total to the next link. Cart never asks "which
discounts apply" - it just hands the subtotal to the head of the chain and takes back
whatever comes out the other end.
- 1Checkout starts from the subtotal of every line item, before any discount.
- 2The subtotal enters the chain at its first link.
- 3The coupon's output becomes the voucher's input - order matters.
- 4FreeShippingRule only zeroes shipping if the running total already clears its threshold.
- 5The last link's output is the number the customer actually pays.
CartItem pairs a Product with a quantity; it is deliberately the only place quantity
lives; Product itself is immutable catalog data shared across every cart that references
it.
Class diagram
Code
Design decisions
- Discounts are Chain of Responsibility, not Strategy. A
FeeStrategy-style "pick one and run it" doesn't fit here because real carts stack discounts - a coupon and a loyalty discount can both be active at once. Chaining each rule's output into the next rule's input is what lets three discounts combine into one answer without any of them knowing the other two exist. - Each
PriceRuledecides its own eligibility.applytakes the whole cart, not just a number, so a rule like "free shipping over $50" can inspect the subtotal itself rather thanCartpre-filtering which rules even get a turn. That keeps eligibility logic next to the rule it governs instead of scattered across a dispatcher. - Chain order is an explicit
List<PriceRule>built by whoever assembles the cart, not a priority field on each rule. A percentage coupon applied before a flat voucher gives a different final total than the reverse order; making the order a visible list, rather than a number buried in each rule, is what makes that behavior reviewable at a glance. - What's missing for a real system: rules here mutate a running total but a production system needs per-line-item discount attribution for receipts and returns, and stacking rules should probably be capped (a "max one coupon" business rule) rather than left unlimited - both are storefront policy, not core cart mechanics, so they're left out.