Design an e-commerce platform (Amazon / Flipkart)
The interviewer asks
“Design the core of an e-commerce marketplace like Amazon or Flipkart: users browse products, add them to a cart, check out and pay, and track their orders. Many sellers list products and ship from several warehouses.”
What a strong answer looks like
A strong answer scopes the platform into bounded services and spends its time on the order path. Browsing is read-heavy and served from caches, a CDN and a search index fed by CDC from the catalogue. The cart is a per-user key-value document. Checkout is the hard part: it re-prices the cart, reserves inventory with a short TTL, creates an order in a PENDING state with an idempotency key, and hands off to the payment service; payment success or failure drives the order through a state machine via events, with a saga that releases stock or refunds on failure. The order database is relational and sharded by user or order ID; downstream work (seller notification, invoicing, shipping, emails) is async via Kafka using the transactional outbox. Strong candidates give numbers for normal days vs Big Billion Days, never oversell, never double charge, and explain what is strongly consistent (stock, money, order state) and what can be eventually consistent (search, recommendations, reviews).
Concepts to mention
1 Requirements
Functional
- Browse categories, search products and view product pages with price, offers and delivery date
- Add, update and remove items in a cart that persists across devices
- Checkout: choose address, see final price with taxes and delivery fee, pay (UPI, cards, wallets, COD)
- Create orders that may split into shipments from different sellers and warehouses
- Order tracking and history; cancel and return flows
- Sellers manage listings, price and stock, and receive orders
Non-functional
- Product page p99 under 200 ms; checkout API p99 under 1 s excluding the payment provider
- Normal ~5k orders/min; sale peak ~20x (Big Billion Days first hour)
- No overselling of stock and no double charging, even with retries
- Order and payment data strongly consistent and durable (RPO ~0)
- Availability 99.95% for browse and 99.99% target for checkout
- Data for Indian users stored in India (payment data under RBI localisation rules)
Out of scope (say it out loud)
- Search ranking details (see product-search)
- Recommendations (see recommendation-system)
- Warehouse management internals (see inventory-management)
- Payment gateway internals (see payment-gateway)
- Last-mile logistics
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
GET/v1/products/{id}?pincode=560001 | Product page: details, price, offers, stock status and delivery estimate for the pincode |
PUT/v1/cart/items/{sku_id} | Set quantity of an item in the user's cart (0 removes) |
POST/v1/checkout | Validate cart, re-price, reserve stock, create a PENDING order; header Idempotency-Key; returns order_id and payment session |
POST/v1/payments/callback | Webhook from payment service/gateway with the payment result (signed, idempotent) |
GET/v1/orders/{order_id} | Order status with shipments and tracking |
POST/v1/orders/{order_id}/cancel | Cancel items not yet shipped; triggers refund and stock release |
4 Data model
catalog_productsMySQL/Postgres sharded, or a document storeproduct_idtitlebrandcategoryattributes JSONimages[]seller_idSource of truth for listings; cached heavily in Redis and CDN; streamed to search via CDC.
listing_offersPostgres + Redis cachesku_idseller_idpricemrpstatusPrice is read from here at checkout, never trusted from the client.
cartsRedis (with persistence) or DynamoDB/Cassandrauser_id (key) -> {items: [{sku_idqtyadded_priceadded_at}]updated_at}Simple key-value access by user. Guest carts keyed by device ID are merged at login.
ordersPostgres/MySQL sharded by user_id (or Vitess)order_id (PK, time-ordered ID)user_idstatustotalcurrencyaddress_ididempotency_key (unique per user)payment_idcreated_atversionRelational: needs transactions with order_items and outbox rows. Shard by user_id so 'my orders' is a single-shard query; order_id encodes the shard.
order_items / shipmentsSame shard as ordersorder_iditem_idsku_idseller_idwarehouse_idqtyunit_priceshipment_iditem_statusItems are grouped into shipments by seller and warehouse.
outboxSame database as ordersevent_idaggregate_idtypepayloadcreated_atpublishedWritten in the same transaction as the order change; a relay (Debezium) publishes to Kafka.
inventoryInventory service DB (Postgres) + Redis counters for hot SKUssku_idwarehouse_idavailablereservedSee inventory-management. Reservations have a TTL (~10-15 min).
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Checkout and pay
- 1User taps 'Place order'; the app sends POST /v1/checkout with an Idempotency-Key generated once for this attempt.
- 2Checkout service loads the cart, re-reads current prices and offers from the server side, recomputes taxes and delivery fee and shows a changed price if anything moved.
- 3It asks the inventory service to reserve each SKU at a chosen warehouse (conditional decrement of available, increment of reserved) with a 15-minute TTL.
- 4In one DB transaction it inserts the order (status PENDING_PAYMENT), order items, and an outbox event OrderCreated; the unique (user_id, idempotency_key) index makes a retried request return the same order.
- 5It creates a payment session with the payment service using order_id as the merchant reference and returns the payment details to the app.
- 6User pays via UPI/card; the payment service sends a signed webhook. Checkout verifies it, and in one transaction moves the order to CONFIRMED (only if still PENDING_PAYMENT, checked by version) and writes an OrderConfirmed outbox event.
- 7Consumers of OrderConfirmed: inventory converts reservations into committed deductions, the seller service notifies sellers, invoicing generates GST invoices, notifications send SMS/push, and fulfilment creates shipments with logistics partners.
- 8If payment fails or the reservation TTL expires first, the order moves to CANCELLED and an event releases the reserved stock; a late successful payment for a cancelled order triggers an automatic refund.
View a product page
- 1Images and static assets come from the CDN.
- 2Gateway calls the product page aggregator, which fetches product details (Redis cache, ~95% hit), the best offer and price, stock status and the delivery promise for the pincode in parallel.
- 3Each dependency has a timeout; if offers or delivery estimates are slow, the page renders without them rather than failing.
- 4Response is assembled and returned in under ~150 ms.
7 Deep dives
The order state machine and the checkout saga
An order touches services that cannot share one database transaction: inventory, payment, order, and later fulfilment. Use a saga: a sequence of local transactions with compensations. The order service owns a state machine: CREATED -> PENDING_PAYMENT -> CONFIRMED -> PACKED -> SHIPPED -> DELIVERED, with side branches CANCELLED, PAYMENT_FAILED, RETURN_REQUESTED, REFUNDED. Every transition is a conditional update (UPDATE ... WHERE id = ? AND status = 'PENDING_PAYMENT' AND version = ?), so two concurrent events cannot both win. Compensations: payment failed -> release reservation; reservation expired before payment -> cancel order and auto-refund any late payment; seller cannot fulfil -> cancel that item, refund, and offer an alternative seller. Orchestration (the order service or a Temporal workflow drives steps) is easier to reason about and debug for checkout than pure choreography. Every external call carries the order_id as an idempotency key, so retries after timeouts are safe. Finally, run a reconciliation job that compares orders with payment provider settlement reports every few hours to catch anything a lost webhook missed.
Idempotency and the double-click problem
Mobile networks in India drop requests often. A user taps 'Pay', the request reaches the server, the response is lost, and the app retries. Without protection you get two orders and maybe two charges. Three layers stop this. First, the client creates an Idempotency-Key per checkout attempt and resends it on retry; the server stores (user_id, key) with a unique constraint and returns the stored response for repeats. Second, payment calls use order_id as the merchant reference so the payment provider rejects a second payment intent for the same order. Third, the webhook handler is idempotent: it records payment_id as processed and ignores duplicates, because providers deliver webhooks at least once. Also handle the case where the user gives up and retries with a new key: the app shows the existing pending order instead of creating a new one when the cart has not changed. Saying 'exactly-once delivery does not exist end to end; we make every step idempotent' is what interviewers want to hear.
Reserving stock without overselling
Stock must never go below zero, but locking a row for the whole payment (which can take minutes with UPI) would kill throughput. So checkout reserves: a single atomic conditional update such as UPDATE inventory SET available = available - :qty, reserved = reserved + :qty WHERE sku_id = :sku AND warehouse_id = :wh AND available >= :qty. If zero rows change, the item is out of stock at that warehouse and the service tries the next nearest warehouse. A reservation row records order_id, quantity and expires_at. On payment success the reservation is committed (reserved decreases, the item is gone); on failure or expiry a sweeper releases it back to available. For a few extremely hot SKUs during sales, a single row becomes a hot spot; those are handled by the flash-sale design (Redis counters, token buckets, queues). Mention that 'add to cart' does not reserve stock: carts are wish lists, and reserving at cart time lets bots lock up inventory.
Service boundaries and data ownership
Split by business capability, each service owning its data: catalogue (listings, content), pricing and offers, inventory, cart, order, payment, fulfilment, user and address, seller. Do not let services read each other's tables; they call APIs or consume events. Reads that combine data (the product page, 'my orders' with tracking) are built by an aggregator or by read models fed from events, so the order service is not called with heavy joins for every page view. Consistency needs differ: money, order state and stock need strong consistency inside their owner; search, recommendations, review counts and analytics can lag by seconds or minutes. The transactional outbox keeps events and state changes atomic without two-phase commit. In an interview, do not draw 30 microservices; draw the 5-6 that matter for the order path and say the rest exist.
8 Trade-offs
SQL vs NoSQL for orders
Orders need multi-row transactions (order, items, outbox), constraints (unique idempotency key) and flexible queries for support tools, so a relational DB sharded by user_id (or a distributed SQL like Vitess/TiDB) fits. Carts and sessions are simple key lookups, where Redis or DynamoDB is cheaper.
Orchestrated vs choreographed saga
Orchestration puts the checkout logic in one place with clear state and timeouts, ideal for the critical path. Choreography (services react to events) is fine for side effects like emails, invoices and analytics, where no central decision is needed.
Reserve at checkout vs at add-to-cart vs at payment
Reserving at add-to-cart lets carts hoard stock; reserving only after payment risks taking money for items that are gone. Reserve at checkout with a short TTL; refund automatically in the rare race.
Monolith first vs microservices
A modular monolith is faster to build for a startup; at Flipkart scale independent scaling and deploys of catalogue, cart and order justify services. Mention the cost: distributed transactions and harder debugging.
9 Bottlenecks
Sale-day traffic
20x spikes at fixed times. Pre-scale stateless services and database capacity, warm caches, put a virtual waiting room in front of checkout, and turn off non-essential features (recommendations, reviews) with flags to protect the order path.
Hot SKUs in inventory
A single phone launch can get 100k checkout attempts per second on one row. Move that SKU to a Redis-based counter with admission control; see the flash-sale design.
Order DB write load
Shard by user_id; keep transactions small; move status-history writes and analytics to async consumers. Order lookup by order_id works because the ID encodes the shard.
10 Failure handling
Payment webhook never arrives
Order stays PENDING_PAYMENT. A poller queries the payment service for orders pending more than a few minutes, and the reconciliation job checks settlement files. The reservation TTL is longer than the payment window to avoid cancelling a paid order.
Kafka or consumers down
Order state is already committed with an outbox row; events are published when the relay recovers. Downstream steps are delayed, not lost. Consumers are idempotent by event_id.
Inventory service unavailable
Checkout cannot safely reserve, so it fails fast with a clear message rather than overselling; browse and cart keep working. Circuit breakers stop retry storms.
11 What impresses
- Stating which data needs strong consistency and which can be eventual
- Explaining the late-payment-after-cancel race and auto refund
- Idempotency at client, order and payment webhook levels
- Reconciliation with the payment provider as a safety net
- Server-side re-pricing at checkout; never trusting client prices
- Load-shedding non-critical features to protect checkout during sales
12 Mistakes
- Using a single distributed transaction (2PC) across inventory, payment and orders
- Reserving stock at add-to-cart
- No idempotency, so retries create duplicate orders or charges
- Drawing dozens of microservices without explaining the order path
- Assuming the payment webhook always arrives exactly once
- Reading another service's database directly
13 Follow-ups
Lessons this design uses
Practice questions
- The product detail page of your e-commerce app gets 50,000 requests per second during a sale and the database is struggling. What would you cache, and how?
- You are designing an e-commerce app like Flipkart. Which data needs strong consistency and which can be eventually consistent?
- The business wants active-active writes in Mumbai and Hyderabad regions for an order system. What consistency options do you have and which would you choose?
- You are building the order service for a new e-commerce app. A teammate wants MongoDB because 'it scales'. Which database would you pick and why?
- In your order schema, should the order line store just product_id, or also the product name and price? Explain.
- In your checkout flow, which steps would you do synchronously in the request and which would you move to a queue?
- You must shard the orders table of an e-commerce site. Candidates are order_id, buyer_id and seller_id. Which do you pick and why?
- In your e-commerce flow, the payment succeeds but creating the order fails because inventory ran out at the last second. The customer has paid and has no order. How should the system handle this?
- Your product page service calls reviews, pricing and recommendations. Recommendations becomes slow, and suddenly product pages fail completely. Why, and how do you design against it?
- Placing an order reserves inventory, charges the customer, and books a delivery slot, each in a different service. The delivery booking fails after payment succeeded. How do you design this so the system ends up consistent?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.