Designing an E-Commerce Platform
E-commerce platforms like Amazon, Shopify, and eBay must handle massive product catalogs, concurrent shopping sessions, real-time inventory tracking, and enormous traffic spikes during events like Black Friday. The system design must prioritize availability, data consistency for inventory and orders, and low-latency search.
Scale Requirements
- 100 million+ products in the catalog
- 50 million daily active users
- 10,000+ orders per second during peak
- 99.99% availability for browse and search
- Strong consistency for inventory and payment
- Sub-200ms page load for product pages
Product Catalog Design
The product catalog is the foundation of any e-commerce system. It must support diverse product types with varying attributes, fast search, and efficient browsing through categories.
// Flexible product schema supporting varied attributes
interface Product {
id: string;
sellerId: string;
title: string;
slug: string;
description: string;
category: CategoryPath; // e.g., ["Electronics", "Phones", "Smartphones"]
brand: string;
basePrice: number;
currency: string;
images: ProductImage[];
variants: ProductVariant[];
attributes: Record<string, string | number | boolean>;
status: "DRAFT" | "ACTIVE" | "ARCHIVED";
createdAt: Date;
updatedAt: Date;
}
interface ProductVariant {
sku: string;
attributes: Record<string, string>; // e.g., { color: "Red", size: "M" }
price: number;
inventoryCount: number;
weight: number;
dimensions: { length: number; width: number; height: number };
}
// Category as a tree (materialized path)
type CategoryPath = string[];
// Stored as: "Electronics > Phones > Smartphones"
// Enables efficient queries: find all products under "Electronics"For storage, use PostgreSQL for the relational product data and Elasticsearch for full-text search and faceted filtering. Keep them synchronized with a CDC (Change Data Capture) pipeline or event-driven updates.
Shopping Cart Architecture
The shopping cart must be fast, resilient, and handle concurrent access. There are two primary approaches: server-side carts and client-side carts.
| Approach | Pros | Cons |
|---|---|---|
| Server-side (Redis/DB) | Persists across devices, supports analytics, enables abandoned cart recovery | Higher server load, latency on every operation |
| Client-side (localStorage) | Zero server load, instant operations, works offline | Lost on browser clear, no cross-device sync, harder to validate |
| Hybrid | Best of both worlds, local for speed, server for persistence | Complexity of sync logic, conflict resolution needed |
interface CartItem {
productId: string;
variantSku: string;
quantity: number;
priceAtAdd: number; // Snapshot price when item was added
addedAt: Date;
}
interface ShoppingCart {
userId: string;
items: CartItem[];
updatedAt: Date;
expiresAt: Date; // Auto-expire abandoned carts
}
class CartService {
private redis: RedisClient;
async addItem(userId: string, item: CartItem): Promise<ShoppingCart> {
const cartKey = `cart:${userId}`;
// Check inventory before adding
const available = await inventoryService.checkAvailability(
item.variantSku,
item.quantity
);
if (!available) throw new Error("Item out of stock");
// Get current cart
const cart = await this.getCart(userId);
// Check if item already exists - update quantity
const existingIndex = cart.items.findIndex(
(i) => i.variantSku === item.variantSku
);
if (existingIndex >= 0) {
cart.items[existingIndex].quantity += item.quantity;
} else {
cart.items.push(item);
}
cart.updatedAt = new Date();
cart.expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
await this.redis.setex(cartKey, 7 * 86400, JSON.stringify(cart));
return cart;
}
async getCart(userId: string): Promise<ShoppingCart> {
const data = await this.redis.get(`cart:${userId}`);
if (!data) return { userId, items: [], updatedAt: new Date(), expiresAt: new Date() };
return JSON.parse(data);
}
}Inventory Management
Inventory management is one of the most challenging aspects of e-commerce. The system must prevent overselling while maximizing availability. This requires careful concurrency control.
The Overselling Problem
If 100 users try to buy the last item simultaneously, naive implementations might let all 100 succeed. You must use atomic operations or pessimistic locking to prevent this. The most common approach is to reserve inventory at checkout time and release it if the order is not completed within a timeout.
class InventoryService {
// Atomic inventory reservation using database-level locking
async reserveInventory(
items: { sku: string; quantity: number }[],
orderId: string,
ttlMinutes: number = 15
): Promise<boolean> {
const client = await db.pool.connect();
try {
await client.query("BEGIN");
for (const item of items) {
// SELECT FOR UPDATE locks the row
const result = await client.query(
`UPDATE inventory
SET available_count = available_count - $1,
reserved_count = reserved_count + $1
WHERE sku = $2
AND available_count >= $1
RETURNING available_count`,
[item.quantity, item.sku]
);
if (result.rowCount === 0) {
await client.query("ROLLBACK");
// Release any previously reserved items in this batch
await this.releaseReservation(orderId);
return false;
}
// Record the reservation for timeout handling
await client.query(
`INSERT INTO inventory_reservations (order_id, sku, quantity, expires_at)
VALUES ($1, $2, $3, NOW() + INTERVAL '$4 minutes')`,
[orderId, item.sku, item.quantity, ttlMinutes]
);
}
await client.query("COMMIT");
return true;
} catch (error) {
await client.query("ROLLBACK");
throw error;
} finally {
client.release();
}
}
// Background job: release expired reservations
async releaseExpiredReservations(): Promise<void> {
const expired = await db.query(
`SELECT * FROM inventory_reservations WHERE expires_at < NOW()`
);
for (const reservation of expired.rows) {
await db.query(
`UPDATE inventory
SET available_count = available_count + $1,
reserved_count = reserved_count - $1
WHERE sku = $2`,
[reservation.quantity, reservation.sku]
);
await db.query(
`DELETE FROM inventory_reservations WHERE id = $1`,
[reservation.id]
);
}
}
}Order Processing Pipeline
Order processing is an asynchronous, multi-step pipeline. Using an event-driven approach ensures reliability and allows each step to be retried independently.
enum OrderStatus {
CREATED = "CREATED",
PAYMENT_PENDING = "PAYMENT_PENDING",
PAYMENT_CONFIRMED = "PAYMENT_CONFIRMED",
PREPARING = "PREPARING",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED",
CANCELLED = "CANCELLED",
REFUNDED = "REFUNDED",
}
// Order processing as an event-driven pipeline
async function handleOrderCreated(order: Order): Promise<void> {
// 1. Validate order (prices, availability)
await validateOrder(order);
// 2. Reserve inventory
const reserved = await inventoryService.reserveInventory(
order.items.map((i) => ({ sku: i.sku, quantity: i.quantity })),
order.id
);
if (!reserved) {
await updateOrderStatus(order.id, OrderStatus.CANCELLED, "Out of stock");
await notifyCustomer(order.customerId, "order_cancelled_stock");
return;
}
// 3. Process payment
await updateOrderStatus(order.id, OrderStatus.PAYMENT_PENDING);
const paymentResult = await paymentService.charge(order);
if (!paymentResult.success) {
await inventoryService.releaseReservation(order.id);
await updateOrderStatus(order.id, OrderStatus.CANCELLED, "Payment failed");
return;
}
// 4. Confirm order
await updateOrderStatus(order.id, OrderStatus.PAYMENT_CONFIRMED);
await inventoryService.confirmReservation(order.id);
// 5. Send to fulfillment
await fulfillmentService.createShipment(order);
await notifyCustomer(order.customerId, "order_confirmed");
}Search and Recommendations
Product search and recommendations are critical for conversion. Search must support full-text queries, faceted filtering (brand, price range, ratings), autocomplete, and typo tolerance.
- Search engine: Elasticsearch or OpenSearch for full-text search with relevance scoring
- Autocomplete: Use edge n-gram tokenizers for prefix matching as users type
- Faceted search: Aggregate queries in Elasticsearch for filters like price ranges and categories
- Personalization: Re-rank search results based on user history and preferences
- Recommendations: Collaborative filtering ("customers who bought X also bought Y") and content-based filtering (similar product attributes)
Scaling for High Traffic (Black Friday)
Peak shopping events can see 10-100x normal traffic. The system must be prepared well in advance.
| Strategy | Implementation |
|---|---|
| CDN for static assets | Cache product images, CSS, JS on CloudFront/Cloudflare edge |
| Read replicas | Scale read-heavy product catalog queries across multiple DB replicas |
| Queue-based order processing | Buffer orders in a queue so the backend processes at a sustainable rate |
| Auto-scaling | Pre-warm instances before the event, auto-scale based on CPU and request count |
| Circuit breakers | Degrade non-essential features (recommendations, reviews) under extreme load |
| Rate limiting | Protect against bots and abuse with per-user rate limits |
Architecture Summary
A well-designed e-commerce platform separates concerns into independent services (catalog, cart, inventory, order, payment, search), uses caching aggressively for read-heavy workloads, ensures strong consistency only where required (inventory and payments), and degrades gracefully under extreme load. The key insight is that browsing and searching can tolerate eventual consistency, while inventory and payment operations must be strongly consistent.