Definition
- User – the customer with an account and payment methods.
- Product – carries a name, price (
BigDecimal), and available stock. - Cart – holds a list of
CartItemobjects, each with a product and quantity. - CartItem – references one Product and a quantity; contributes price times quantity to the subtotal.
- DiscountStrategy – a pluggable interface for flat, percentage, and buy-one-get-one promotions.
- CheckoutService – the atomic flow that validates inventory, applies discounts, charges payment, and decrements stock.
Think of the shopping cart as a payment basket at National Bank of India's online store. Rajesh adds items to his basket, each with a quantity and price. When he checks out, the system totals the basket, applies any coupon he has, confirms the items are actually in stock, charges his account, and reduces inventory. The cart is just a running tally until checkout turns it into a real transaction.
Understanding the Concepts
What are the core entities?
The core entities are User, Product, Cart, CartItem, DiscountStrategy, and CheckoutService.
package com.bank.cart.model;
import java.math.BigDecimal;
public class Product {
private final String productId;
private final String name;
private final BigDecimal price;
private int stock;
public Product(String productId, String name,
BigDecimal price, int stock) {
this.productId = productId;
this.name = name;
this.price = price;
this.stock = stock;
}
public synchronized boolean decrementStock(int quantity) {
if (stock < quantity) return false;
stock -= quantity;
return true;
}
public String getProductId() { return productId; }
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
public int getStock() { return stock; }
}
package com.bank.cart.model;
import java.math.BigDecimal;
public class CartItem {
private final Product product;
private int quantity;
public CartItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public BigDecimal getLineTotal() {
return product.getPrice().multiply(
BigDecimal.valueOf(quantity));
}
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}
How does the Cart work?
A Cart holds items and computes a running subtotal.
package com.bank.cart.model;
import java.math.BigDecimal;
import java.util.*;
public class Cart {
private final String userId;
private final Map<String, CartItem> items;
public Cart(String userId) {
this.userId = userId;
this.items = new LinkedHashMap<>();
}
public void addItem(Product product, int quantity) {
items.merge(product.getProductId(),
new CartItem(product, quantity),
(existing, newItem) -> {
existing.setQuantity(
existing.getQuantity() + quantity);
return existing;
});
System.out.println("Added " + quantity + " x "
+ product.getName() + " to cart");
}
public void removeItem(String productId) {
items.remove(productId);
}
public BigDecimal getSubtotal() {
return items.values().stream()
.map(CartItem::getLineTotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public Collection<CartItem> getItems() {
return items.values();
}
public boolean isEmpty() { return items.isEmpty(); }
}
How does the DiscountStrategy work?
Discounts are pulled into a strategy interface so marketing can launch new promotions without touching checkout code.
package com.bank.cart.service;
import com.bank.cart.model.Cart;
import java.math.BigDecimal;
public interface DiscountStrategy {
BigDecimal applyDiscount(Cart cart, BigDecimal subtotal);
}
package com.bank.cart.service;
import com.bank.cart.model.Cart;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PercentageDiscount implements DiscountStrategy {
private final BigDecimal percentage;
public PercentageDiscount(BigDecimal percentage) {
this.percentage = percentage;
}
@Override
public BigDecimal applyDiscount(Cart cart, BigDecimal subtotal) {
BigDecimal discount = subtotal.multiply(percentage)
.divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
System.out.println("Applied " + percentage + "% discount: -"
+ discount);
return subtotal.subtract(discount);
}
}
package com.bank.cart.service;
import com.bank.cart.model.Cart;
import java.math.BigDecimal;
public class FlatDiscount implements DiscountStrategy {
private final BigDecimal flatAmount;
public FlatDiscount(BigDecimal flatAmount) {
this.flatAmount = flatAmount;
}
@Override
public BigDecimal applyDiscount(Cart cart, BigDecimal subtotal) {
BigDecimal discounted = subtotal.subtract(flatAmount);
if (discounted.compareTo(BigDecimal.ZERO) < 0) {
discounted = BigDecimal.ZERO;
}
System.out.println("Applied flat discount: -" + flatAmount);
return discounted;
}
}
How does the checkout flow work?
Checkout is the critical, must-be-atomic flow. It validates inventory, applies discounts, charges payment, and decrements stock.
package com.bank.cart.service;
import com.bank.cart.model.*;
import java.math.BigDecimal;
public class CheckoutService {
private final PaymentStrategy paymentStrategy;
private final DiscountStrategy discountStrategy;
public CheckoutService(PaymentStrategy paymentStrategy,
DiscountStrategy discountStrategy) {
this.paymentStrategy = paymentStrategy;
this.discountStrategy = discountStrategy;
}
public BigDecimal checkout(Cart cart, String userId) {
if (cart.isEmpty()) {
throw new IllegalStateException("Cart is empty");
}
// Step 1: Validate inventory
for (CartItem item : cart.getItems()) {
if (item.getProduct().getStock() < item.getQuantity()) {
throw new IllegalStateException(
"Insufficient stock for "
+ item.getProduct().getName());
}
}
// Step 2: Apply discounts
BigDecimal subtotal = cart.getSubtotal();
BigDecimal total = discountStrategy.applyDiscount(
cart, subtotal);
// Step 3: Charge payment
boolean paid = paymentStrategy.charge(userId, total);
if (!paid) {
throw new IllegalStateException("Payment failed");
}
// Step 4: Decrement stock (only after payment succeeds)
for (CartItem item : cart.getItems()) {
if (!item.getProduct().decrementStock(
item.getQuantity())) {
// Refund and abort
paymentStrategy.refund(userId, total);
throw new IllegalStateException(
"Stock changed during checkout for "
+ item.getProduct().getName());
}
}
System.out.println("Checkout complete for " + userId
+ ", total: " + total);
return total;
}
}
Added 2 x Gold Coin to cart
Added 1 x Silver Bar to cart
Applied 10% discount: -350.00
Payment charged: 3150.00
Checkout complete for RAJESH-001, total: 3150.00
Why does the authoritative inventory check happen at checkout?
Stock can sell out between adding to the cart and checking out. The inventory check when adding to the cart is advisory only. The authoritative check and decrement happen at checkout under a lock or atomic operation.
// Product.decrementStock is synchronized
public synchronized boolean decrementStock(int quantity) {
if (stock < quantity) return false;
stock -= quantity;
return true;
}
How does payment work?
Payment goes through a strategy interface supporting card, UPI, and wallet.
package com.bank.cart.service;
import java.math.BigDecimal;
public interface PaymentStrategy {
boolean charge(String userId, BigDecimal amount);
void refund(String userId, BigDecimal amount);
}
What about the race between cart and checkout?
The subtle correctness concern is the race between adding to the cart and checking out, because an item can sell out in between. The cart's display subtotal is advisory; the checkout total is authoritative. The inventory decrement at checkout uses a synchronised or atomic operation.
What about extensibility?
Adding a new promotion type (buy-one-get-one) is a new DiscountStrategy class. Adding a new payment method is a new PaymentStrategy class. Neither changes the checkout flow.
What should a senior engineer know?
- All money is
BigDecimalto avoid floating-point rounding. - The checkout flow ordering matters: validate inventory first, charge payment second, decrement stock third. If any step fails, nothing downstream is affected.
- In production, the inventory decrement is a database-level atomic operation (e.g.,
UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?). - The discount strategy can be composed: apply a percentage discount, then a flat coupon, using a chain of strategies.
What are common interview follow-up questions?
- How do you handle concurrent checkout for the same limited-stock item?
- How would you add a "buy one get one free" promotion?
- How do you handle payment failure after stock is already decremented?
- How would you implement a wishlist feature?
- How does the system handle partial refunds on order cancellation?
Visual Overview
Checkout Flow (must be atomic):
───────────────────────────────
1. Validate inventory for every item
│
▼ all in stock?
2. Apply DiscountStrategy to subtotal
│
▼
3. PaymentStrategy.charge(userId, total)
│
├── fails → abort (no stock deducted)
│
▼ success
4. Product.decrementStock(quantity)
│
├── fails → PaymentStrategy.refund()
│
▼ success
5. Order created, cart cleared
Cart Structure: ─────────────── Cart (RAJESH-001) ├── CartItem: Gold Coin x 2 ₹1500 x 2 = ₹3000 ├── CartItem: Silver Bar x 1 ₹500 x 1 = ₹500 │ Subtotal: ₹3500 │ 10% discount: -₹350 └────────────────────────────── Total: ₹3150
Key Takeaway
When you design an online shopping cart, model a User holding a Cart of cart items, where each item references a Product and a quantity, and the cart computes a running subtotal. The cart itself is just a tally; the real work and the real risk live in the checkout flow.
Discounts are the first place to apply good design. Pull coupon logic into a discount-strategy interface with implementations for flat, percentage, and buy-one-get-one promotions, so marketing can launch a new promotion as a new class rather than forcing a change to the checkout code. This is the Strategy pattern and the Open/Closed Principle together.
Checkout is the critical, must-be-atomic flow. It validates inventory for every item, applies discounts to compute the authoritative final total, charges payment, and only then decrements stock. The ordering matters: if payment fails, no stock is deducted, and if stock is insufficient, no payment is taken, so the system never ends up half-committed.
The subtle correctness concern is the race between adding to the cart and checking out, because an item can sell out in between. The authoritative inventory check and decrement happen at checkout under a lock or atomic operation, never when the item is first added. All money is BigDecimal. At National Bank of India, this is the same transactional discipline applied to any purchase of a limited asset like gold coins, where the inventory check, payment, and stock deduction must succeed or fail together as one unit.
Remember one sentence: a shopping cart totals items, applies discount strategies, and atomically validates inventory, charges payment, and decrements stock at checkout.
---