Design a Shopping Cart
Cart items, pluggable discount/pricing rules, and a checkout pipeline with tax and coupons.
Problem Statement
Design a shopping cart for an e-commerce application. Customers can add products, change quantities, apply a coupon or discount policy, and checkout to produce an immutable order with subtotal, discount, tax, and final total.
The important design decision is separation of concerns: the cart owns item selection and quantity state, while CheckoutService owns price calculation. Discounts are pluggable strategies so a new coupon rule is additive rather than another conditional inside the cart.
Business context
Shopping Cart appears frequently in Amazon, Flipkart, Adobe, and marketplace interviews because it looks familiar but quickly exposes design quality. Candidates must model money safely, keep cart mutation independent from pricing, support coupon variation, and create a checkout boundary that can later integrate payments, inventory reservation, tax engines, and order fulfillment.
A strong solution treats the cart as a domain aggregate, not a calculator. Pricing becomes a separate service composed with Strategy, making promotions testable and replaceable without risking item-management logic.
Functional Requirements
Represent products with id, name, unit price, and tax category.
Add a product to the cart with a positive quantity.
Merge repeated additions of the same product by increasing the line-item quantity.
Update or remove a product line by product id.
Expose a read-only snapshot of current cart line items.
Apply a coupon or discount policy to the cart without changing cart item state.
Calculate subtotal, discount, tax, and grand total during checkout.
Produce an immutable order containing the purchased items and pricing breakdown.
Notify interested observers when the cart changes.
Non-Functional Requirements
Pricing correctness
Use precise decimal money arithmetic and round only at clear money boundaries, never with floating-point doubles.
Separation of concerns
Cart manages selection and quantities; CheckoutService computes totals; discount policies live behind DiscountStrategy.
Extensibility
New coupons, tax plans, cart observers, and checkout integrations should be added as classes, not by editing a giant checkout conditional.
Concurrency awareness
Cart mutations and checkout snapshots must be atomic enough that an order is priced from one consistent item view.
Auditability
Checkout should capture subtotal, discount, tax, total, timestamp, and item snapshot so later price changes do not rewrite historical orders.
Requirement Clarification
QCan a product appear multiple times in the cart?
No duplicate lines in the base design. The cart stores one CartItem per product id and increments quantity on repeated add.
QCan multiple coupons stack?
Base design applies one active DiscountStrategy. Stacking can be added by a composite discount strategy or a promotion engine extension.
QDo we reserve inventory during add-to-cart?
No. Cart is customer intent, not inventory reservation. Inventory reservation belongs at checkout or payment authorization time.
QIs tax per product category or a flat rate?
Use a flat percentage tax in the reference implementation to keep focus on LLD seams. Product tax category is retained for future category-aware tax strategies.
QShould checkout clear the cart?
The base implementation returns an Order and leaves clearing as an application decision. Production systems usually clear after payment succeeds, not merely after price calculation.
UML Class Diagram
Sequence Diagram
Entity Identification
Product
Immutable catalog value used by cart lines. It owns product identity, display name, unit price, and a tax category hook for future tax policies.
CartItem
Immutable line item combining one Product with a positive quantity. It can derive its own line subtotal from product price and quantity.
Cart
Aggregate root for customer intent. It owns the product-id to CartItem map, selected discount strategy, and observer list, but does not finalize totals.
DiscountStrategy
Coupon policy seam. Implementations compute discount amount from cart context and subtotal, while checkout clamps the result for safety.
CartObserver
Notification seam for UI badges, analytics, or persistence hooks whenever the cart changes.
CheckoutService
Pricing boundary. It takes a consistent cart snapshot, sums line items, applies discount strategy, computes tax, and returns an Order.
Order
Immutable checkout result containing item snapshot and money breakdown. It is safe to persist or send to payment because it no longer depends on mutable cart state.
Design Patterns Used
DiscountStrategy isolates coupon behavior. Percentage, fixed-amount, no-discount, and future rule-based promotions implement one method while checkout remains unchanged.
Cart acts as the aggregate composite over many CartItem leaves. Clients treat the cart as one purchasable selection even though totals come from individual lines.
CartObserver lets UI counters, analytics, and persistence hooks react to item or coupon changes without being coupled to cart mutation methods.
Static factory methods such as Product.of and DiscountStrategy.percentage create validated domain objects and hide concrete coupon classes from callers.
Step-by-Step Design
1Model products as immutable catalog values
A product should not change because a customer modifies the cart. Keep identity and unit price immutable, and create products through a validating factory method.
public static Product of(String id, String name, String unitPrice, String taxCategory) { return new Product(id, name, new BigDecimal(unitPrice), taxCategory); }2Make cart lines immutable and quantity-based
CartItem represents one product plus quantity. Updating a quantity returns a new line, which avoids accidental mutation leaking through cart snapshots.
public CartItem withQuantity(int newQuantity) { return new CartItem(product, newQuantity); } public BigDecimal lineSubtotal() { return product.getUnitPrice().multiply(BigDecimal.valueOf(quantity)); }3Let the cart own item composition, not checkout math
Cart merges repeated product additions, updates quantities, removes lines, and exposes a safe snapshot. It stores the active discount policy but does not compute final tax or total.
4Represent coupons as strategies
Every promotion implements DiscountStrategy. Checkout asks the strategy for a discount amount and then normalizes it so no coupon can produce a negative total.
public interface DiscountStrategy { BigDecimal apply(Cart cart, BigDecimal subtotal); String description(); }5Put subtotal, discount, tax, and total in CheckoutService
The checkout boundary snapshots items, sums line subtotals, invokes the discount strategy, computes tax, and creates the order. This keeps pricing tests focused on one service.
BigDecimal subtotal = calculateSubtotal(snapshot); BigDecimal discount = normalizeDiscount(strategy.apply(cart, subtotal), subtotal); BigDecimal tax = taxableAmount.multiply(taxRatePercent) .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);6Return an immutable order snapshot
An Order stores item copies and the final money breakdown at checkout time. Later product price updates or cart edits must not rewrite the order history.
Complete Java Implementation
Explanation of Every Class
Product
Immutable catalog value with validated id, name, unit price, and tax category. Product.of is the factory method used by clients instead of calling a constructor directly.
CartItem
Immutable line item. It combines a Product and quantity, validates positive quantity, and computes the line subtotal with precise decimal arithmetic.
DiscountStrategy
Strategy interface for all coupon policies. Its static factory methods hide concrete discount classes and keep client code small.
NoDiscountStrategy
Null-object style strategy that returns a zero discount. It lets the cart always hold a strategy and avoids null checks during checkout.
PercentageDiscountStrategy
Coupon implementation for percentage discounts such as SUMMER10. It validates the rate and computes a rounded percentage of subtotal.
FixedAmountCouponStrategy
Coupon implementation for fixed-value discounts. It caps the discount at subtotal so a coupon cannot make the taxable amount negative.
CartObserver
Observer interface for cart change events. UI counters, analytics, and persistence listeners can subscribe without changing cart logic.
LoggingCartObserver
Small observer implementation used by the demo. It prints the event and current item count whenever the cart changes.
Cart
Composite aggregate of CartItem objects. It merges adds, updates quantities, removes items, stores the active discount strategy, and emits observer notifications.
CheckoutService
Pricing boundary. It snapshots the cart, calculates subtotal, asks the strategy for a discount, computes tax, and returns an immutable order.
Order
Immutable checkout result with item snapshot and money breakdown. It protects historical order pricing from later cart or product changes.
Main
Runnable demonstration that creates products, mutates the cart, applies a percentage coupon, checks out with tax, and prints the totals.
Dry Run
Sample input
Products: headphones at 100.00 and mouse at 50.00. Actions: add headphones x2, add mouse x1, apply SUMMER10 percentage coupon, checkout with 8% tax.
| Step | Cart action | Subtotal | Discount | Tax | Order total |
|---|---|---|---|---|---|
| 1 | Add headphones x2 | 200.00 | 0.00 | 0.00 | Cart updated |
| 2 | Add mouse x1 | 250.00 | 0.00 | 0.00 | Cart updated |
| 3 | Apply SUMMER10 | 250.00 | 25.00 | 0.00 | Policy selected |
| 4 | Checkout with 8% tax | 250.00 | 25.00 | 18.00 | 243.00 |
Checkout prices a single snapshot: subtotal 250.00, discount 25.00, taxable amount 225.00, tax 18.00, and final total 243.00.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| addProduct | O(1) | O(1) | LinkedHashMap lookup by product id, then insert or replace one immutable line item. |
| updateQuantity / removeProduct | O(1) | O(1) | Product id lookup and one map mutation. |
| items snapshot | O(N) | O(N) | Copies N line items into an immutable list view. |
| checkout | O(N) | O(N) | Sums N line items and stores an order snapshot. Simple discount and tax policies are O(1). |
| notify observers | O(M) | O(M) | Copies and calls M observers for a cart event. |
The hot path is checkout over N cart lines. Coupon strategies shown here are constant-time, but a production promotion engine may depend on product categories, customer segments, or external eligibility checks.
Extensibility
New coupon type
Add a new DiscountStrategy implementation, such as buy-one-get-one, category percentage, loyalty points, or threshold discount.
Stacked discounts
Introduce a composite discount strategy that owns a list of strategies and applies them in a documented order with a maximum cap.
Category-based tax
Replace the flat tax rate with a TaxStrategy that reads Product.taxCategory and calculates tax per line item.
Inventory reservation
Add an inventory service at checkout so selected quantities are reserved only when the customer commits to purchase.
Cart persistence
Add a repository or event log that stores cart changes per customer while keeping the in-memory cart model unchanged.
Alternative Designs
Pricing methods directly on Cart
Put subtotal, discount, tax, and total methods on Cart so clients call one object for everything.
Tradeoffs
Simpler for tiny demos but mixes item mutation with pricing rules. Tax, promotions, audit, and payment integration will make Cart too large.
Promotion engine with rules
Replace individual discount classes with a rule engine that evaluates eligibility predicates and reward actions.
Tradeoffs
More flexible for large marketplaces, but overkill for an interview base design and harder to reason about than Strategy.
Mutable CartItem lines
Let CartItem expose a setter for quantity and mutate the same line object in place.
Tradeoffs
Uses fewer allocations, but snapshots become unsafe because external code could still hold a line reference and mutate it after checkout.
Checkout as a Facade over many services
Model checkout as a larger orchestration layer over tax, payment, inventory, shipping, and order repositories.
Tradeoffs
Closer to production, but it can distract from the core LLD question unless the interviewer explicitly asks for external integrations.
Common Mistakes
- ×
Putting discount and tax calculations inside Cart, which makes item mutation and pricing policy change for different reasons.
- ×
Using double for money and accumulating rounding errors across line items and tax.
- ×
Allowing duplicate line items for the same product id, then producing inconsistent quantity updates.
- ×
Letting coupon code conditionals grow inside checkout instead of using DiscountStrategy.
- ×
Returning mutable cart internals so callers can alter items without observer events or validation.
- ×
Creating an order that references the live cart instead of an immutable item and price snapshot.
- ×
Applying tax before discount without clarifying the business rule.
Follow-up Interview Questions
QHow would you support multiple coupons on one cart?
Create a composite discount strategy that contains ordered child strategies. It applies each child to the remaining subtotal, records the breakdown, and enforces a cap.
QHow would you add category-specific tax?
Introduce a TaxStrategy that calculates tax per CartItem using Product.taxCategory. CheckoutService depends on the interface, similar to discounts.
QHow do you prevent stale prices if the catalog changes after items are added?
Store a price snapshot on CartItem or validate prices during checkout. For orders, always persist the final item prices used at purchase time.
QWhere should payment authorization live?
Outside this domain model. A payment adapter should consume the produced Order or a checkout quote, authorize payment, then persist and confirm the order.
QWhat happens if inventory is unavailable at checkout?
Checkout should call an inventory reservation service before payment capture. If reservation fails, return a recoverable error and keep the cart editable.
Production Considerations
Money and currency
Use a money type with currency instead of bare BigDecimal. The sample focuses on decimal correctness but omits multi-currency conversion rules.
Promotion audit trail
Record coupon code, strategy name, eligibility result, and discount amount on the order for customer support and finance reconciliation.
Concurrency and sessions
Persist cart state with versioning so two browser tabs cannot overwrite each other silently. Use optimistic locking on cart updates.
External tax engines
Real commerce systems often call external tax services by address, product category, and jurisdiction. Keep that behind a tax strategy or adapter.
Observability
Track coupon usage, checkout failures, abandoned carts, tax-service latency, and average order value to detect business and reliability issues.
What Interviewers Look For
Did the candidate keep Cart focused on items and quantity state?
Is discount behavior represented by Strategy rather than coupon conditionals?
Are money values precise and rounded intentionally?
Does checkout produce an immutable order snapshot rather than referencing live cart state?
Can the design extend to tax engines, inventory reservation, and payment without rewriting core cart logic?
Does the candidate name tradeoffs around coupon stacking and price snapshots?
Quiz
0/5 answered
1.Why should **CheckoutService** calculate totals instead of **Cart**?
2.Which pattern best fits coupons like 10% off or 50.00 off?
3.What does **Cart** model in the Composite pattern?
4.Why does **Order** copy the item list?
5.What is the role of **CartObserver**?
Practice Variants
Add category coupons
IntermediateImplement a DiscountStrategy that gives 15% off only for products in a specified tax or catalog category.
Stack discounts safely
AdvancedBuild a composite discount strategy that applies multiple coupons in order and returns a detailed discount breakdown.
Reserve inventory at checkout
IntermediateAdd an inventory reservation collaborator to CheckoutService so checkout fails gracefully when requested quantity is unavailable.
Flashcards
Cheat Sheet
Entities: Product, CartItem, Cart, DiscountStrategy, CartObserver, CheckoutService, Order.
Patterns: Strategy for discounts, Composite for cart lines, Observer for cart events, Factory Method for validated object creation.
Flow: add/update/remove items in Cart → apply a DiscountStrategy → CheckoutService snapshots items → subtotal → discount → tax → immutable Order.
Boundaries: Cart owns item state. CheckoutService owns pricing. Order owns history.
Invariants: one line per product id; positive quantities; non-negative money; discount never exceeds subtotal; order never references mutable cart state.
Complexity: cart mutations O(1), checkout O(N), observer notification O(M).
Extend: new coupon = new strategy; tax rules = tax strategy; stacked coupons = composite strategy; inventory/payment = checkout collaborators.
References
- BookHead First Design Patterns (Strategy and Observer) — Freeman & Robson
- BookEffective Java — Static factories and BigDecimal guidance — Joshua Bloch
- DocsRefactoring Guru — Strategy Pattern