Courses › System Design › Question Breakdowns

Design an Online Bookstore

Cached catalog and search, carts, reserve-at-checkout inventory and an idempotent checkout saga.

Design an online bookstore like the early Amazon: customers search and browse a catalog of millions of books, add them to a cart, check out and track their orders, while inventory stays accurate across warehouses. It's a classic e-commerce question about catalog reads at scale, search, carts, inventory without overselling and an order workflow that spans payment and fulfillment.

Requirements

Functional

  • Customers can search (title, author, ISBN, keywords) and browse books by category, and view a book's details, price and availability
  • Customers can add to cart and check out (pay, choose shipping)
  • Customers can see order status and history
  • Inventory is tracked across warehouses; customers can't buy what isn't in stock
  • Customers can leave reviews and ratings

Out of scope: recommendations (mention as an extension), third-party sellers, ebooks, returns logistics.

Non-functional

  • Scale: ~20M books, ~50M daily visitors; reads dominate (browse:order ≈ 100:1); ~200 orders/second at peak (holiday sales ×10)
  • Fast browsing: pages in < 200ms; search in < 300ms
  • No overselling and no double charging — orders and inventory need strong consistency
  • Highly available browsing and cart, even if checkout degrades

[!TIP] Different parts, different consistency The catalog is read-heavy and can be slightly stale (cache it everywhere). The cart needs durability but not strict consistency. Inventory and orders need transactions. Saying this out loud lets you choose a different store and caching strategy for each instead of one database for everything.

Core Entities

  • Book — ISBN, title, authors, description, categories, price, images
  • Inventory — book, warehouse, quantity on hand, quantity reserved
  • Cart — user (or anonymous session), items (book, quantity)
  • Order — user, items, prices at purchase time, address, payment status, fulfillment status
  • Review — user, book, rating, text

API

GET  /v1/search?q=distributed+systems&author=...&cursor=...
GET  /v1/books/{isbn}                        → details + price + availability
PUT  /v1/cart/items/{isbn}   { "quantity": 2 }
POST /v1/checkout   Idempotency-Key: ...   { "addressId": "a1", "paymentMethod": "pm_tok_..." }
→ 201 { "orderId": "ord_5", "status": "confirmed" }
GET  /v1/orders/{orderId}

High-Level Design

user = client "Web / App" @0,1
cdn = cache "CDN\n(images, pages)" @1,0
cat = service "Catalog Service" @1,1
catdb = store "Catalog DB" @2,0
search = service "Search\n(Elasticsearch)" @2,1
cart = service "Cart Service" @1,2
cartdb = store "Cart Store\n(DynamoDB)" @2,2
order = service "Order Service" @3,1
inv = store "Inventory + Orders\n(Postgres)" @4,1
pay = service "Payment Provider" @4,0
ful = queue "Fulfillment Events" @4,2
user -> cdn
user -> cat
cat -> catdb
cat -> search
user -> cart
cart -> cartdb
cart -> order : checkout
order -> inv : reserve stock
order -> pay
order -> ful
  • Browse: book pages come from the catalog service, heavily cached (CDN for images and largely static page parts, Redis for book details).
  • Search: an Elasticsearch index built from the catalog DB via change data capture.
  • Cart: a key-value store keyed by user/session; cheap to update, durable.
  • Checkout: the order service reserves inventory, charges the payment provider, creates the order, and emits events to fulfillment.

Deep Dives

1. Catalog reads

20M books × ~5 KB of metadata is only ~100 GB — one Postgres primary with read replicas can hold it. The challenge is read volume, solved by caching (see Scaling Reads):

  • CDN for images and the static parts of book pages.
  • Redis cache-aside for book details; prices and availability are cached with short TTLs (or fetched separately so the main page can be cached longer).
  • Bestsellers are naturally hot and stay in cache; the long tail falls through to replicas.

2. Search

Relational LIKE queries can't do relevance, typos or facets. Index books in Elasticsearch: title and author with analyzers (stemming, typo tolerance), categories and price as facets, popularity as a ranking signal. Keep it in sync with CDC from the catalog DB (Debezium → Kafka → indexer); a few seconds of lag is fine. (See Elasticsearch.)

3. Inventory without overselling

At checkout, decrement stock atomically:

UPDATE inventory
SET reserved = reserved + :qty
WHERE isbn = :isbn AND warehouse = :w AND on_hand - reserved >= :qty;

0 rows updated → out of stock at that warehouse; try the next one. Reservations become permanent when the order is paid and are released if payment fails or the order times out (e.g. 15 minutes).

[!WARNING] Don't reserve on "add to cart" Carts are abandoned most of the time. Reserving at add-to-cart locks stock for people who never buy. Show availability (possibly stale) on the page and in the cart, and reserve only at checkout. For a hyped release (a new Harry Potter at midnight), add a short hold with a timer, as in Ticketmaster.

4. Checkout as a saga

Checkout touches inventory, payment and fulfillment — separate systems, no single transaction. Orchestrate the steps with compensation:

[Create order: pending] -> [Reserve inventory] -> [Charge payment] -> [Confirm order] -> {Fulfillment events}
[Charge payment] -> [Release inventory on failure]
  • The client sends an idempotency key so a double-clicked "Place order" creates one order.
  • Prices are copied into the order at checkout, so later price changes don't alter it.
  • Payment calls carry their own idempotency key (see Design a Payment System).

5. Carts

Store carts in a key-value store (DynamoDB / Redis with persistence) keyed by user ID, or by a session ID for anonymous users. On login, merge the anonymous cart into the user's. Carts tolerate eventual consistency; recompute prices and availability at checkout.

6. Flash sales and holiday peaks

  • Cache aggressively and serve browse traffic even if checkout is overloaded.
  • Put checkout behind a queue or a rate limiter so the inventory DB sees a steady load; tell users "processing" rather than failing.
  • Shard inventory by ISBN if one primary can't keep up; a single hot title stays on one row, so batch its reservations or pre-split stock into buckets.

7. Reviews and ratings

Store reviews in their own table/service. Maintain an aggregate (count, sum) per book updated asynchronously, and cache the average on the book page. One review per (user, book), enforced by a unique constraint.

What's expected at each level

  • Mid-level: catalog, cart and order services with a sensible schema, a search index, and atomic stock decrement.
  • Senior: different consistency per component, caching tiers for the catalog, CDC into search, reserve-at-checkout with timeouts, idempotent checkout.
  • Staff: a checkout saga with compensation, flash-sale protection and hot-SKU handling, multi-warehouse allocation, and graceful degradation (browse stays up when checkout struggles).