Cart/CartItem/PricingEngine/DiscountStrategy/Checkout classes, stacking multiple discounts via Strategy+Decorator, and the inventory-check-timing tradeoff.
Published September 23, 2026
class CartItem { Product product; int quantity; }
class Cart { List<CartItem> items; }
interface DiscountStrategy { double apply(double currentTotal); }
class PricingEngine { double calculateTotal(Cart cart, List<DiscountStrategy> discounts); }
class Checkout { Cart cart; PricingEngine pricingEngine; }
class PercentageOffDiscount implements DiscountStrategy {
private final double percentage;
public double apply(double currentTotal) { return currentTotal * (1 - percentage / 100.0); }
}
class FlatAmountDiscount implements DiscountStrategy {
private final double amount;
public double apply(double currentTotal) { return Math.max(0, currentTotal - amount); }
}
class PricingEngine {
double calculateTotal(Cart cart, List<DiscountStrategy> discounts) {
double total = cart.items.stream().mapToDouble(i -> i.product.price * i.quantity).sum();
for (DiscountStrategy discount : discounts) {
total = discount.apply(total); // each discount wraps/transforms the running total — Decorator's chaining shape
}
return total;
}
}
Each individual DiscountStrategy is a Strategy (a swappable discount algorithm), but applying a list of them in sequence, each transforming the previous result, is structurally the same chaining idea as Decorator (see Decorator Pattern) — just applied to a running numeric total instead of wrapped objects. This combination is worth naming explicitly: a single PercentageOffDiscount alone is plain Strategy; stacking a percentage discount and a flat-amount coupon and a loyalty discount, each applied to the previous step's result, is where the Decorator-style chaining becomes the relevant structural insight — and order matters: a 10% discount then a flat $5 off produces a different total than $5 off then 10%, which is exactly the kind of detail worth calling out unprompted, since it's a real business decision, not an implementation footgun to hide.
At add-to-cart: reserving stock the moment an item is added prevents a user from later discovering it's unavailable at checkout (better UX), but risks holding inventory hostage for abandoned carts (the same over-reservation problem Design an Inventory Management System's expiry discussion addresses) — especially costly for popular items during a flash sale. At checkout only: no premature reservation, but a user can add items to their cart and only discover at checkout that stock ran out in the meantime (worse UX, but simpler and doesn't tie up inventory for carts that never convert). Most production e-commerce systems land on a hybrid: no hard reservation at add-to-cart (maybe a soft, non-binding availability check), with an actual reservation-with-expiry (the InventoryService pattern) triggered specifically at checkout initiation, balancing both concerns rather than picking one extreme.
Q: How would you prevent a malicious/buggy client from submitting negative quantities or manipulating price client-side? A: Price must always be looked up server-side from Product (never trusted from client input), and quantity should be validated against a sane positive bound — Cart/CartItem as shown intentionally store only quantity and a Product reference, not a client-suppliable price field, which is itself a deliberate design choice worth stating rather than assuming.
Q: Should DiscountStrategy order be caller-controlled or fixed by the system? A: Depends on the business rule being modeled — some discount types (a loyalty-program discount) might be defined to always apply last regardless of what other coupons are stacked; this argues for either a fixed, documented ordering convention or explicit priority values on each DiscountStrategy rather than leaving order purely to whatever sequence a caller happens to pass the list in.
Q: How does this design connect to In-Memory Rate Limiter's concerns during a flash sale? A: A flash sale's checkout flow is exactly where per-user or global rate limiting on 'add to cart'/'begin checkout' calls becomes relevant — protecting the inventory-reservation hot path from being overwhelmed connects this LLD exercise directly to the E-Commerce Checkout & Inventory at Scale system design case's flash-sale discussion.
Q: What happens if PricingEngine.calculateTotal() is called twice with the same discounts list but the underlying cart changed between calls? A: It recomputes from scratch each time (stateless, no cached total) — this is a deliberate simplicity choice; a production system might cache the computed total and invalidate it on cart mutation for performance, but recomputing on demand avoids an entire class of stale-total bugs that a cached-and-invalidated design would need to guard against carefully.