Requirements & Scope
Functional Requirements
- Browse products: View product listings by category, featured, deals
- Search/filter: Full-text search with filters (price, brand, rating, availability)
- Add to cart: Add items, modify quantities, apply coupons
- Checkout: Payment processing, address selection, delivery options
- Track orders: Real-time order status updates
- Reviews/ratings: Post-purchase reviews and ratings
Non-Functional Requirements
- High availability: 99.99% uptime, multi-region deployment
- Fast search: Search results in under 200ms
- Inventory consistency: Prevent overselling, real-time stock updates
- Flash sale handling: Support 100x traffic spikes during sales
- Scalability: Handle billions of products and millions of concurrent users
Scale Estimation
| Metric | Daily | Per Second |
|---|---|---|
| Product views | 2B | ~23K |
| Search queries | 500M | ~6K |
| Orders | 10M | ~115 |
| Cart operations | 100M | ~1.2K |
| Reviews | 1M | ~12 |
Core Entities
- Product, ProductVariant, Category, Brand, User, Cart, CartItem, Order, OrderItem, Inventory, Review, Address, Payment
Product Catalog & Search
Product Catalog Architecture
@Service
public class ProductService {
@Autowired private ProductRepository mysqlRepo;
@Autowired private SearchRepository elasticsearchRepo;
@Autowired private CacheService cacheService;
@Autowired private ImageService imageService;
public ProductDTO getProduct(Long productId) {
String cacheKey = "product:" + productId;
return cacheService.getOrLoad(cacheKey, () -> {
Product product = mysqlRepo.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
// Get images from CDN
List<String> images = imageService.getProductImages(productId);
// Get variants
List<ProductVariant> variants = mysqlRepo
.findVariantsByProductId(productId);
return ProductDTO.builder()
.id(product.getId())
.name(product.getName())
.description(product.getDescription())
.price(product.getPrice())
.images(images)
.variants(variants)
.rating(product.getAvgRating())
.reviewCount(product.getReviewCount())
.build();
}, Duration.ofMinutes(10));
}
public List<ProductDTO> searchProducts(SearchRequest request) {
// Build Elasticsearch query
BoolQueryBuilder query = QueryBuilders.boolQuery();
if (request.getKeyword() != null) {
query.must(QueryBuilders.multiMatchQuery(request.getKeyword(),
"name", "description", "brand")
.fuzziness(Fuzziness.AUTO));
}
// Apply filters
if (request.getCategoryId() != null) {
query.filter(QueryBuilders.termQuery("categoryId",
request.getCategoryId()));
}
if (request.getMinPrice() != null) {
query.filter(QueryBuilders.rangeQuery("price")
.gte(request.getMinPrice()));
}
if (request.getMaxPrice() != null) {
query.filter(QueryBuilders.rangeQuery("price")
.lte(request.getMaxPrice()));
}
if (request.getBrands() != null && !request.getBrands().isEmpty()) {
query.filter(QueryBuilders.termsQuery("brand", request.getBrands()));
}
if (request.getMinRating() != null) {
query.filter(QueryBuilders.rangeQuery("rating")
.gte(request.getMinRating()));
}
// Sort
SortBuilder sortBuilder = switch (request.getSortBy()) {
case PRICE_LOW -> SortBuilders.fieldSort("price").order(SortOrder.ASC);
case PRICE_HIGH -> SortBuilders.fieldSort("price").order(SortOrder.DESC);
case RATING -> SortBuilders.fieldSort("rating").order(SortOrder.DESC);
case POPULARITY -> SortBuilders.fieldSort("salesCount").order(SortOrder.DESC);
default -> SortBuilders.scoreSort();
};
SearchRequest esRequest = new SearchRequest("products")
.source(new SearchSourceBuilder()
.query(query)
.sort(sortBuilder)
.from(request.getPage() * request.getSize())
.size(request.getSize())
.highlighter(new HighlightBuilder()
.field("name")
.field("description")));
SearchResponse response = elasticsearchClient.search(esRequest,
RequestOptions.DEFAULT);
return parseResults(response);
}
}
Search Data Model
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(500),
description TEXT,
category_id BIGINT REFERENCES categories(id),
brand VARCHAR(255),
price DECIMAL(10,2),
avg_rating DECIMAL(2,1),
review_count INT,
sales_count INT,
is_active BOOLEAN,
created_at TIMESTAMP,
INDEX idx_category (category_id),
INDEX idx_brand (brand),
INDEX idx_price (price)
);
CREATE TABLE product_variants (
id BIGINT PRIMARY KEY,
product_id BIGINT REFERENCES products(id),
sku VARCHAR(100) UNIQUE,
attributes JSON,
price DECIMAL(10,2),
stock_quantity INT,
INDEX idx_product (product_id)
);
CREATE TABLE categories (
id BIGINT PRIMARY KEY,
name VARCHAR(255),
parent_id BIGINT REFERENCES categories(id),
level INT,
INDEX idx_parent (parent_id)
);
Elasticsearch Index
{
"mappings": {
"properties": {
"id": { "type": "long" },
"name": { "type": "text", "analyzer": "standard" },
"description": { "type": "text", "analyzer": "standard" },
"categoryId": { "type": "long" },
"brand": { "type": "keyword" },
"price": { "type": "double" },
"rating": { "type": "float" },
"salesCount": { "type": "long" },
"inStock": { "type": "boolean" },
"createdAt": { "type": "date" }
}
}
}
Cart & Checkout
Cart Service
@Service
public class CartService {
@Autowired private CartRepository cartRepo;
@Autowired private CacheService cacheService;
private static final Duration CART_TTL = Duration.ofDays(30);
public Cart getCart(String userId) {
String cacheKey = "cart:" + userId;
return cacheService.getOrLoad(cacheKey, () -> {
return cartRepo.findByUserId(userId)
.orElseGet(() -> createNewCart(userId));
}, CART_TTL);
}
@Transactional
public Cart addToCart(String userId, AddToCartRequest request) {
Cart cart = getCart(userId);
// Check inventory before adding
Inventory inventory = inventoryService.checkStock(
request.getProductId(), request.getQuantity());
if (!inventory.isAvailable()) {
throw new InsufficientStockException();
}
// Check if item already in cart
Optional<CartItem> existingItem = cart.getItems().stream()
.filter(item -> item.getProductId().equals(request.getProductId())
&& item.getVariantId().equals(request.getVariantId()))
.findFirst();
if (existingItem.isPresent()) {
CartItem item = existingItem.get();
int newQuantity = item.getQuantity() + request.getQuantity();
// Validate total quantity against stock
if (newQuantity > inventory.getAvailableQuantity()) {
throw new InsufficientStockException();
}
item.setQuantity(newQuantity);
item.setUpdatedAt(Instant.now());
} else {
cart.getItems().add(CartItem.builder()
.productId(request.getProductId())
.variantId(request.getVariantId())
.quantity(request.getQuantity())
.addedAt(Instant.now())
.build());
}
cart.setUpdatedAt(Instant.now());
cartRepo.save(cart);
// Invalidate cache
cacheService.invalidate("cart:" + userId);
return cart;
}
@Transactional
public Cart updateQuantity(String userId, Long itemId, int newQuantity) {
Cart cart = getCart(userId);
CartItem item = cart.getItems().stream()
.filter(i -> i.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new CartItemNotFoundException());
if (newQuantity <= 0) {
cart.getItems().remove(item);
} else {
// Validate stock
Inventory inventory = inventoryService.checkStock(
item.getProductId(), newQuantity);
if (!inventory.isAvailable()) {
throw new InsufficientStockException();
}
item.setQuantity(newQuantity);
item.setUpdatedAt(Instant.now());
}
cart.setUpdatedAt(Instant.now());
cartRepo.save(cart);
cacheService.invalidate("cart:" + userId);
return cart;
}
}
Checkout Flow
@Service
public class CheckoutService {
@Autowired private OrderService orderService;
@Autowired private PaymentService paymentService;
@Autowired private InventoryService inventoryService;
@Autowired private ShippingService shippingService;
@Transactional
public Order checkout(CheckoutRequest request) {
String userId = request.getUserId();
Cart cart = cartService.getCart(userId);
if (cart.isEmpty()) {
throw new EmptyCartException();
}
// 1. Reserve inventory (prevents overselling)
List<InventoryReservation> reservations = new ArrayList<>();
try {
for (CartItem item : cart.getItems()) {
InventoryReservation reservation = inventoryService
.reserve(item.getProductId(), item.getQuantity());
reservations.add(reservation);
}
} catch (InsufficientStockException e) {
// Rollback all reservations
reservations.forEach(r -> inventoryService.release(r.getId()));
throw e;
}
// 2. Calculate totals
BigDecimal subtotal = calculateSubtotal(cart.getItems());
BigDecimal shipping = shippingService.calculateShipping(
cart.getItems(), request.getShippingAddress());
BigDecimal tax = calculateTax(subtotal, request.getShippingAddress());
BigDecimal discount = applyCoupons(cart.getCouponCodes(), subtotal);
BigDecimal total = subtotal.add(shipping).add(tax).subtract(discount);
// 3. Create order
Order order = Order.builder()
.userId(userId)
.items(convertToOrderItems(cart.getItems()))
.subtotal(subtotal)
.shipping(shipping)
.tax(tax)
.discount(discount)
.total(total)
.shippingAddress(request.getShippingAddress())
.status(OrderStatus.PLACED)
.build();
order = orderService.createOrder(order);
// 4. Process payment with idempotency
PaymentResult payment = paymentService.charge(
order.getId(), total, request.getPaymentMethod(),
request.getIdempotencyKey());
if (!payment.isSuccessful()) {
// Release inventory reservations
reservations.forEach(r -> inventoryService.release(r.getId()));
orderService.updateStatus(order.getId(), OrderStatus.PAYMENT_FAILED);
throw new PaymentFailedException(payment.getErrorMessage());
}
// 5. Confirm order and inventory
reservations.forEach(r -> inventoryService.confirm(r.getId()));
orderService.updateStatus(order.getId(), OrderStatus.CONFIRMED);
// 6. Clear cart
cartService.clearCart(userId);
// 7. Send confirmation
notificationService.sendOrderConfirmation(order);
return order;
}
}
Payment Data Model
CREATE TABLE payments (
id BIGINT PRIMARY KEY,
order_id BIGINT REFERENCES orders(id),
amount DECIMAL(10,2),
currency VARCHAR(3),
status ENUM('PENDING','PROCESSING','COMPLETED','FAILED','REFUNDED'),
payment_method VARCHAR(50),
stripe_payment_id VARCHAR(255),
idempotency_key VARCHAR(255) UNIQUE,
created_at TIMESTAMP,
completed_at TIMESTAMP,
INDEX idx_order (order_id),
INDEX idx_idempotency (idempotency_key)
);
Order Management & Fulfillment
Order State Machine
PLACED → CONFIRMED → PROCESSING → SHIPPED → DELIVERED → COMPLETED
↓ ↓ ↓ ↓ ↓ ↓
CANCELLED CANCELLED CANCELLED RETURNED RETURNED RATED
Inventory Management
@Service
public class InventoryService {
@Autowired private InventoryRepository inventoryRepo;
@Autowired private CacheService cacheService;
@Autowired private KafkaTemplate<String, String> kafkaTemplate;
public InventoryReservation reserve(Long productId, int quantity) {
// Use pessimistic locking to prevent overselling
Inventory inventory = inventoryRepo.findByIdWithLock(productId)
.orElseThrow(() -> new InventoryNotFoundException(productId));
int available = inventory.getQuantity() - inventory.getReserved();
if (available < quantity) {
throw new InsufficientStockException(productId, quantity, available);
}
inventory.setReserved(inventory.getReserved() + quantity);
inventoryRepo.save(inventory);
InventoryReservation reservation = InventoryReservation.builder()
.productId(productId)
.quantity(quantity)
.status(ReservationStatus.ACTIVE)
.expiresAt(Instant.now().plus(Duration.ofMinutes(15)))
.build();
inventoryRepo.saveReservation(reservation);
// Invalidate cache
cacheService.invalidate("stock:" + productId);
return reservation;
}
public void confirm(InventoryReservation reservation) {
Inventory inventory = inventoryRepo.findById(reservation.getProductId())
.orElseThrow();
inventory.setQuantity(inventory.getQuantity() - reservation.getQuantity());
inventory.setReserved(inventory.getReserved() - reservation.getQuantity());
inventoryRepo.save(inventory);
reservation.setStatus(ReservationStatus.CONFIRMED);
inventoryRepo.saveReservation(reservation);
cacheService.invalidate("stock:" + reservation.getProductId());
}
public void release(InventoryReservation reservation) {
Inventory inventory = inventoryRepo.findById(reservation.getProductId())
.orElseThrow();
inventory.setReserved(inventory.getReserved() - reservation.getQuantity());
inventoryRepo.save(inventory);
reservation.setStatus(ReservationStatus.RELEASED);
inventoryRepo.saveReservation(reservation);
cacheService.invalidate("stock:" + reservation.getProductId());
}
// Scheduled job to expire old reservations
@Scheduled(fixedRate = 60000)
public void expireStaleReservations() {
List<InventoryReservation> expired = inventoryRepo
.findExpiredReservations(Instant.now());
for (InventoryReservation reservation : expired) {
release(reservation);
kafkaTemplate.send("inventory-releases",
reservation.getProductId().toString());
}
}
}
Flash Sale Handling
@Service
public class FlashSaleService {
@Autowired private QueueService queueService;
@Autowired private InventoryService inventoryService;
@Autowired private CacheService cacheService;
private static final int VIRTUAL_WAITING_ROOM_SIZE = 10000;
public FlashSaleResult processFlashSaleOrder(FlashSaleOrderRequest request) {
Long productId = request.getProductId();
// 1. Check if product is in flash sale
FlashSale sale = cacheService.get("flashsale:" + productId);
if (sale == null || !sale.isActive()) {
throw new NotInFlashSaleException(productId);
}
// 2. Check inventory from cache (fast check)
Integer stock = cacheService.get("flashstock:" + productId);
if (stock == null || stock <= 0) {
return FlashSaleResult.soldOut();
}
// 3. Add to virtual waiting room queue
String queueId = queueService.enqueue("flashsale:" + productId,
request.getUserId(),
Duration.ofMinutes(10));
// 4. Wait in queue (polling or WebSocket notification)
int position = queueService.getPosition(queueId);
if (position > VIRTUAL_WAITING_ROOM_SIZE) {
return FlashSaleResult.queueFull(position);
}
// 5. When it's user's turn, process order
try {
InventoryReservation reservation = inventoryService
.reserve(productId, request.getQuantity());
// 6. Create order directly (skip normal cart flow)
Order order = orderService.createFlashSaleOrder(request, reservation);
// 7. Process payment
paymentService.charge(order.getId(), order.getTotal(),
request.getPaymentMethod(), request.getIdempotencyKey());
return FlashSaleResult.success(order);
} catch (InsufficientStockException e) {
return FlashSaleResult.soldOut();
} finally {
queueService.dequeue(queueId);
}
}
}
Order Data Model
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
status ENUM('PLACED','CONFIRMED','PROCESSING','SHIPPED',
'DELIVERED','COMPLETED','CANCELLED','RETURNED'),
subtotal DECIMAL(10,2),
shipping DECIMAL(10,2),
tax DECIMAL(10,2),
discount DECIMAL(10,2),
total DECIMAL(10,2),
shipping_address JSON,
billing_address JSON,
created_at TIMESTAMP,
updated_at TIMESTAMP,
shipped_at TIMESTAMP,
delivered_at TIMESTAMP,
INDEX idx_user (user_id, created_at),
INDEX idx_status (status)
);
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT REFERENCES orders(id),
product_id BIGINT REFERENCES products(id),
variant_id BIGINT REFERENCES product_variants(id),
quantity INT,
unit_price DECIMAL(10,2),
total_price DECIMAL(10,2)
);
CREATE TABLE inventory (
product_id BIGINT PRIMARY KEY REFERENCES products(id),
quantity INT,
reserved INT DEFAULT 0,
version INT DEFAULT 0,
INDEX idx_available (quantity, reserved)
);
Scaling Strategy
- Database sharding: Shard by product category or user ID
- Read replicas: Product catalog and order history on read replicas
- CDN: Product images served from CDN (CloudFront)
- Caching: Product details, search results, inventory in Redis
- Async processing: Order confirmation emails, inventory updates via Kafka
Practice Problems
Design a scalable E-Commerce Platform (Design Amazon) system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale E-Commerce Platform (Design Amazon) to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for E-Commerce Platform (Design Amazon) and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. How should inventory be managed during checkout to prevent overselling?
2. What is the best way to implement product search with filters?
3. How should a flash sale handle 100x traffic spike?
4. What happens if payment fails after inventory is reserved?
5. Where should product images be stored?
Flashcards
Question
How does inventory reservation prevent overselling?
Click to reveal answer
Answer
Reserve stock before payment (increment reserved count), confirm after payment (decrement quantity and reserved), release on failure or timeout (decrement reserved).
Question
What search technology should be used for product catalog?
Click to reveal answer
Answer
Elasticsearch for full-text search, filters, aggregations. MySQL for transactional product data. Sync via CDC or async updates.
Question
How does a virtual waiting room work?
Click to reveal answer
Answer
Users are queued when entering sale. Process orders sequentially from queue. Prevents database overload and ensures fair access.
Question
What is idempotency in payment processing?
Click to reveal answer
Answer
Each payment request has a unique idempotency key. If retried (network error), server returns same result without charging twice.
Question
Where should cart data be stored?
Click to reveal answer
Answer
Redis for session-based carts (fast, temporary). Database for persistent carts (linked to user account). TTL-based expiry for abandoned carts.
Question
How should product images be served?
Click to reveal answer
Answer
Store in S3, serve via CloudFront CDN. Generate thumbnails for listing pages. Lazy load images on product detail pages.
Question
What is the checkout order state machine?
Click to reveal answer
Answer
PLACED → CONFIRMED → PROCESSING → SHIPPED → DELIVERED → COMPLETED. Each state transition triggers specific actions and notifications.
Revision Notes
Key Takeaways
- 1.Inventory reservation is critical to prevent overselling
- 2.Elasticsearch provides fast search with complex filters
- 3.Virtual waiting room handles flash sale traffic spikes
- 4.Idempotency prevents duplicate charges in payments
- 5.CDN and caching reduce latency for product images
- 6.Database sharding enables horizontal scaling
Interview Tips
- •Start with functional requirements and scale estimation
- •Draw high-level architecture with all microservices
- •Explain inventory reservation flow in detail
- •Discuss search architecture (MySQL + Elasticsearch)
- •Explain flash sale handling with virtual waiting room
- •Mention payment idempotency for reliability
- •Be ready to discuss database schema and indexing
Cheat Sheet
E-Commerce Platform - Key Points
Architecture Components
- Product Service: Catalog management, pricing
- Search Service: Elasticsearch for full-text search and filters
- Cart Service: Session/persistent cart management
- Order Service: Order lifecycle, state machine
- Inventory Service: Stock management, reservations
- Payment Service: Payment processing, idempotency
Key Design Decisions
- Search: Elasticsearch for fast full-text search with filters
- Inventory: Reservation system prevents overselling
- Cart: Redis for fast access, database for persistence
- Flash Sales: Virtual waiting room + queue-based ordering
- Images: S3 + CDN for scalable media delivery
Scale Numbers
- 2B daily product views (~23K/sec)
- 500M search queries/day (~6K/sec)
- 10M daily orders (~115/sec)
- 100M cart operations/day (~1.2K/sec)
Inventory Flow
Reserve → Payment → Confirm → Decrement
Reserve → Payment Fail → Release
Reserve → Timeout (15min) → Release