Design a Shopify-like E-Commerce Platform¶
Interview Time: 60 min | Difficulty: Medium
Key Focus: Multi-tenant stores, order management, inventory sync, payment processing
Step 1: Functional & Non-Functional Requirements¶
Functional Requirements¶
- Merchants create and manage their own stores (multi-tenant)
- Customers browse products, search by category/name
- Add products to cart, manage wish lists
- Checkout with multiple payment methods (credit card, PayPal, etc.)
- Order management (creation, tracking, fulfillment, returns)
- Inventory management (real-time stock updates, low-stock alerts)
- Product variants (size, color, price variations)
- Order email notifications (order confirmed, shipped, delivered)
- Merchant dashboard (sales report, inventory, order tracking)
- Reviews and ratings on products
Non-Functional Requirements¶
| Requirement | Target | Notes |
|---|---|---|
| Scale | 1M+ shops, 1B+ products, 100K orders/min | Multi-tenant architecture |
| Latency | Product load <100ms, checkout <500ms | Customer-facing critical |
| Availability | 99.95% uptime | SLA required for merchants |
| Consistency | Strong for inventory, eventual for reviews | Inventory must be accurate |
| Throughput | 100K concurrent shoppers per second | Peak traffic during sales |
| Payment | Process 10K payments/sec, PCI compliance | Third-party payment gateways |
Step 2: API Design, Data Model & High-Level Design¶
Core API Endpoints¶
# Product Management
GET /stores/{store_id}/products?page=1&limit=20
→ {products: [{product_id, name, price, variants, stock}]}
POST /stores/{store_id}/products
{name, description, price, category, variants, stock}
→ {product_id}
POST /stores/{store_id}/products/{product_id}/inventory
{variant_id, quantity}
→ {stock_updated: true}
# Shopping
POST /carts/{user_id}/items
{product_id, variant_id, quantity}
→ {cart_id, item_count}
GET /carts/{user_id}
→ {items: [{product_id, quantity, price}], total}
POST /orders
{user_id, store_id, items, shipping_address, payment_method}
→ {order_id, status}
GET /orders/{order_id}
→ {status, items, shipping_info, tracking}
# Reviews
POST /products/{product_id}/reviews
{user_id, rating, comment}
→ {review_id}
GET /products/{product_id}/reviews?sort=recent|helpful
→ {reviews: [{rating, comment, author}]}
Entity Data Model¶
TENANTS (Shops/Stores)
├─ store_id (PK)
├─ owner_id (FK -> users)
├─ store_name (indexed, searchable)
├─ description
├─ logo_url
├─ settings (currency, tax_rate, shipping_zones)
├─ created_at
PRODUCTS
├─ product_id (ULID, PK, sortable)
├─ store_id (FK, partition key)
├─ name, description
├─ price (DECIMAL)
├─ category_id
├─ avg_rating (denormalized)
├─ review_count (denormalized)
├─ created_at, updated_at
PRODUCT_VARIANTS
├─ variant_id (PK)
├─ product_id (FK)
├─ attributes (size, color, etc. as JSON)
├─ variant_price (if different from product)
├─ sku (unique per product)
├─ stock (int, current quantity)
├─ reserved_stock (int, in pending orders)
├─ updated_at
ORDERS
├─ order_id (ULID, PK, sortable)
├─ store_id (FK, partition key)
├─ user_id (FK)
├─ status (pending, paid, shipped, delivered, cancelled)
├─ total_price (DECIMAL)
├─ tax_amount, shipping_cost
├─ payment_status (pending, completed, failed)
├─ created_at, shipped_at, delivered_at
ORDER_ITEMS
├─ order_item_id (PK)
├─ order_id (FK)
├─ product_id (FK)
├─ variant_id (FK)
├─ quantity, unit_price
├─ status (pending, fulfilled, cancelled)
CARTS
├─ user_id + store_id (composite PK)
├─ items [{ variant_id, quantity, price }] (JSON)
├─ total, tax, shipping
├─ expires_at (auto-delete after 30 days)
REVIEWS
├─ review_id (PK)
├─ product_id (FK)
├─ user_id (FK)
├─ rating (1-5)
├─ title, comment
├─ helpful_count, unhelpful_count
├─ created_at
INVENTORY_HISTORY
├─ history_id (PK)
├─ variant_id (FK)
├─ from_quantity, to_quantity
├─ reason (sale, return, adjustment)
├─ order_id (FK, nullable)
├─ created_at
High-Level Architecture¶
graph TB
USER["👤 Customer/Merchant"]
CDN["CDN<br/>(product images)"]
LB["Load Balancer"]
SHOP["Shop Service<br/>(store management)"]
PRODUCT["Product Service<br/>(catalog, variants)"]
INVENTORY["Inventory Service<br/>(stock, reservations)"]
CART["Cart Service<br/>(session mgmt)"]
ORDER["Order Service<br/>(order processing)"]
PAYMENT["Payment Service<br/>(Stripe/PayPal)"]
CACHE["Redis Cluster<br/>(product cache,<br/>cart sessions)"]
DB["PostgreSQL<br/>(multi-tenant,<br/>sharded by store_id)"]
SEARCH["Elasticsearch<br/>(product search)"]
QUEUE["Message Queue<br/>(order events,<br/>notifications)"]
EMAIL["Email Service<br/>(order confirmations)"]
FULFILLMENT["Fulfillment System<br/>(shipping integration)"]
USER --> CDN
USER --> LB
LB --> SHOP
LB --> PRODUCT
LB --> CART
LB --> ORDER
SHOP --> DB
PRODUCT --> CACHE
PRODUCT --> DB
PRODUCT --> SEARCH
INVENTORY --> DB
INVENTORY --> CACHE
CART --> CACHE
CART --> INVENTORY
ORDER --> DB
ORDER --> INVENTORY
ORDER --> QUEUE
PAYMENT --> QUEUE
QUEUE --> EMAIL
QUEUE --> FULFILLMENT
Step 3: Concurrency, Consistency & Scalability¶
🔴 Problem: Inventory Race Condition¶
Scenario: Product has 1 stock. Two users checkout simultaneously, both see stock=1. Double-sale!
Solution: Pessimistic Locking with Reservation
Order Flow (atomic inventory reduction):
1. User adds item to cart → No inventory deduction
(Just update Redis cart session)
2. User clicks "Checkout"
POST /orders {items: [{variant_id, quantity}]}
3. [ATOMIC TRANSACTION]
SELECT stock FROM product_variants
WHERE variant_id = 123
FOR UPDATE; -- Lock this row
IF stock < quantity_requested:
ROLLBACK, return error (out of stock)
ELSE:
UPDATE product_variants
SET stock = stock - quantity_requested
WHERE variant_id = 123;
INSERT INTO orders (...)
INSERT INTO order_items (...)
COMMIT;
Result:
- Only 1 of 2 checkout requests succeeds
- Stock is 100% accurate
- Consistent across all instances
Caching:
cache:product:{variant_id}:stock = 50
→ On checkout, UPDATE DB and invalidate cache immediately
→ Next request refetches fresh stock from DB
🟡 Problem: Inventory Consistency Across Reads¶
Scenario: Merchant dashboard shows 100 remaining units. Customer sees product available. But 50 units are in pending orders that haven't been paid yet.
Solution: Track Reserved Stock Separately
-- Inventory Accounting:
product_variants:
├─ stock: 100 (physically in warehouse)
├─ reserved_stock: 50 (allocated to pending orders)
└─ available: 50 (stock - reserved)
-- When user creates order but payment pending:
UPDATE product_variants
SET reserved_stock = reserved_stock + quantity
WHERE variant_id = 123;
-- When payment confirmed:
UPDATE product_variants
SET stock = stock - quantity,
reserved_stock = reserved_stock - quantity
WHERE variant_id = 123;
-- When payment fails:
UPDATE product_variants
SET reserved_stock = reserved_stock - quantity
WHERE variant_id = 123;
-- (No change to stock, just unreserve)
-- Query for available inventory:
SELECT (stock - reserved_stock) as available
FROM product_variants
WHERE variant_id = 123;
🔷 Problem: Multi-Tenant Data Isolation¶
Scenario: Store A's products appear in Store B's catalog due to isolation bug.
Solution: Partition Database by store_id
PostgreSQL Sharding:
├─ Shop 1 (store_id = 1000-1999)
│ ├─ DB Shard 1 (contains stores with IDs mod 3 = 0)
│ Products, Orders, Inventory
│ └─ Replica 1
│
├─ Shop 2 (store_id = 2000-2999)
│ ├─ DB Shard 2 (contains stores with IDs mod 3 = 1)
│ └─ Replica 2
│
└─ Shop N...
Routing Logic (in application layer):
shard_id = hash(store_id) % 3
connection_string = SHARD_CONFIGS[shard_id]
Every query includes store_id filter:
SELECT * FROM products
WHERE store_id = 1234 AND ...
Index to prevent full-table scans:
CREATE INDEX idx_products_store_id
ON products(store_id)
🟡 Problem: Cart Abandonment & Inventory Locking¶
Scenario: User adds 50 units to cart, leaves page. Stock blocked forever.
Solution: Time-Bounded Reservations
Cart Lifecycle:
1. Add to cart → Create/update Redis session
Key: "cart:{user_id}:{store_id}"
TTL: 30 days (persistent)
2. Start checkout → Reserve inventory
Reserve for 15 minutes (timeout)
Set: "reservation:{order_id}"
TTL: 15 min
3. Payment captured within 15 min:
Finalize purchase, reduce stock
4. Payment fails or timeout:
Background job runs every 1 min:
FOR each expired reservation:
UNRESERVE inventory
SEND email: "Your cart will be cleared in X days"
Step 4: Persistence Layer, Caching & Monitoring¶
Database Design¶
CREATE TABLE stores (
store_id BIGSERIAL PRIMARY KEY,
owner_id BIGINT NOT NULL REFERENCES users(user_id),
store_name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE,
logo_url TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
store_id BIGINT NOT NULL REFERENCES stores(store_id),
name VARCHAR(255) NOT NULL,
description TEXT,
category_id INT,
price DECIMAL(10,2) NOT NULL,
avg_rating DECIMAL(3,2),
review_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_products_store_category
ON products(store_id, category_id);
CREATE INDEX idx_products_store_created
ON products(store_id, created_at DESC);
CREATE TABLE product_variants (
variant_id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(product_id),
sku VARCHAR(100) UNIQUE,
attributes JSONB, -- {color: red, size: M}
variant_price DECIMAL(10,2),
stock INT NOT NULL,
reserved_stock INT DEFAULT 0,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_variants_product
ON product_variants(product_id);
CREATE INDEX idx_variants_sku_store
ON product_variants(sku);
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
store_id BIGINT NOT NULL REFERENCES stores(store_id),
user_id BIGINT NOT NULL REFERENCES users(user_id),
status VARCHAR(50), -- pending, paid, shipped, delivered
total_price DECIMAL(10,2),
tax_amount DECIMAL(10,2),
shipping_cost DECIMAL(10,2),
shipping_address JSONB,
payment_status VARCHAR(50), -- pending, completed, failed
payment_method VARCHAR(50), -- card, paypal
payment_transaction_id VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT NOW(),
shipped_at TIMESTAMP,
delivered_at TIMESTAMP
);
CREATE INDEX idx_orders_store_created
ON orders(store_id, created_at DESC);
CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);
CREATE INDEX idx_orders_status
ON orders(status);
CREATE TABLE order_items (
order_item_id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(order_id),
variant_id BIGINT NOT NULL REFERENCES product_variants(variant_id),
quantity INT NOT NULL,
unit_price DECIMAL(10,2),
status VARCHAR(50)
);
CREATE INDEX idx_order_items_order
ON order_items(order_id);
CREATE TABLE carts (
cart_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
store_id BIGINT NOT NULL REFERENCES stores(store_id),
items JSONB, -- [{variant_id, quantity, price}]
total DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(user_id, store_id)
);
CREATE TABLE reviews (
review_id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(product_id),
user_id BIGINT NOT NULL REFERENCES users(user_id),
rating INT CHECK (rating >= 1 AND rating <= 5),
title VARCHAR(255),
comment TEXT,
helpful_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_reviews_product_rating
ON reviews(product_id, rating DESC);
CREATE INDEX idx_reviews_user
ON reviews(user_id);
Caching Strategy¶
Redis Tier 1: Hot Data (TTL: 5-15 min)
1. Product Cache (catalog)
Key: "product:{product_id}"
Value: {name, price, avg_rating, stock, variants}
TTL: 15 min
Hit rate: 80% (customers view same products)
2. Variant Stock Cache
Key: "stock:{variant_id}"
Value: {stock: 50, reserved: 10, available: 40}
TTL: 5 min
Purpose: Avoid DB hits for every stock check
Strategy: Invalidate immediately after checkout
3. Cart Session
Key: "cart:{user_id}:{store_id}"
Value: {items: [{variant_id, quantity}], total}
TTL: 30 days
Purpose: Shopping cart persistence across sessions
4. Recent Orders (user dashboard)
Key: "orders:{user_id}"
Value: [order_id_1, order_id_2, ...] (latest 5)
TTL: 1 hour
Purpose: Fast order history retrieval
5. Store Hot Products (trending)
Key: "store:{store_id}:trending"
Value: [product_id_1, product_id_2, ...]
TTL: 1 hour
Purpose: Dashboard display
Monitoring & Alerts¶
- alert: CheckoutLatencyHigh
expr: checkout_api_latency_p95 > 1000
annotations: "Checkout latency > 1 second — check payment gateway"
- alert: InventoryMismatch
expr: cache_stock_value != db_stock_value
annotations: "Stock cache out of sync with DB"
- alert: OrderPaymentFailureRate
expr: (failed_payments / total_payments) > 0.05
annotations: "Payment failure rate > 5% — check payment provider"
- alert: MultiTenantDataLeakage
expr: cross_store_query_detected
annotations: "Query returned data from multiple stores — isolation breach!"
- alert: CartAbandonmentRate
expr: (carts_created - successful_orders) / carts_created > 0.85
annotations: "85%+ cart abandonment — check UX or payment friction"
- alert: InventoryReservationStuck
expr: expired_reservations_count > 1000
annotations: "Many expired reservations — cleanup job failing"
Key Metrics:
- Checkout conversion rate — (completed orders / carts created)
- Product load latency p95 — Cache hit rate on product catalog
- Payment success rate — Failed transactions per hour
- Inventory accuracy — (reserved + sold ≈ order count)
- Order fulfillment time — (shipped - created) in hours
- Review submission rate — Engagement metric
⚡ Quick Reference Cheat Sheet¶
Critical Design Decisions¶
- Pessimistic locking —
SELECT FOR UPDATEon checkout ensures inventory consistency - Reserved stock tracking — Separate field for pending orders vs. actual sales
- Store-based sharding — Partition DB by store_id for multi-tenant isolation
- 15-min reservation timeout — Prevents inventory blocking from abandoned carts
- Cache invalidation on stock change — Immediate, not scheduled expiry
- Separate payment gateway — Use Stripe/PayPal APIs, never store credit cards
Tech Stack¶
Frontend: React (product browsing, cart)
Backend: Go/Python (high-throughput order processing)
Database: PostgreSQL (sharded by store_id)
Cache: Redis (product catalog, cart, stock)
Search: Elasticsearch (product search)
Payment: Stripe/PayPal APIs
Email: SendGrid (order notifications)
Fulfillment: ShipStation API (shipping integration)
When to Use What¶
| Problem | Solution |
|---|---|
| Inventory race conditions | Pessimistic locking + atomic transaction |
| Multi-tenant isolation | Database sharding by store_id |
| Slow product queries | Redis cache (TTL 15 min) |
| Abandoned carts | Time-bounded reservations (15 min) |
| Out-of-stock confusion | available = stock - reserved_stock |
🎯 Interview Summary (5 Minutes)¶
- Pessimistic locking →
SELECT FOR UPDATEon inventory table during checkout - Reserved stock → Track separately for pending orders vs. actual sales
- Multi-tenant sharding → Partition database by
store_idto prevent data leaks - 15-minute reservations → Prevent inventory from being blocked by abandoned carts
- Stock cache invalidation → Immediate on sale, not TTL-based (accuracy critical)
- Payment gateway integration → Use dedicated providers (Stripe), never store cards
- Order event queue → Async notifications, fulfillment, email without blocking checkout