Free preview

Interview Walkthrough: An NFR-Led E-Commerce Design

The prompt is one of the most common in the catalog:

"How would you design a scalable and performant e-commerce website that can handle millions of requests per second?"

Engineers usually meet the functional requirements easily and then struggle to achieve scalability and low latency simultaneously. That tension is the entire interview.

Key takeaway

The spine: Scope → FR → NFR → State → API → HLD → Deep Dives. Here the NFRs are not one step among seven — they are the problem. Every architectural decision below is traced back to a number established in step 2.

Step 0 — Scope it before you design

The prompt contains one number ("millions of requests per second") and it is suspiciously round. Interrogate it.

  • Millions of requests per second across what — page views, API calls, product searches, checkouts?
  • How many daily active users, and how spiky? Is there a Black Friday?
  • Read/write ratio? Browsing versus buying is wildly asymmetric.
  • Catalog size? A thousand SKUs and a hundred million SKUs are different systems.
  • Global or one region?
  • What does an hour of downtime cost — and is it worse at 3 a.m. or during a sale?

Then commit to assumptions:

"I'll assume 50 million DAU, mostly browsing: about 3 million requests/sec at peak, of which the overwhelming majority are catalog reads and roughly 5,000/sec are checkouts. Read/write is around 500:1. Catalog is 10 million SKUs, global, with a 10x Black Friday spike. Correct me on any of these."

1. Functional Requirements

Keep it tight:

  • Browse and search the catalog.
  • View a product detail page.
  • Add to cart.
  • Check out and pay.
  • View order history.

Then say the quiet part: "These are all straightforward. What makes this hard is doing the first three at three million requests per second and the fourth without ever overselling inventory or double-charging a card."

2. Non-Functional Requirements — the heart of this problem

Split the system by path, because the two paths have genuinely opposite requirements:

NFRBrowse pathCheckout path
Volume~3M req/sec at peak~5k req/sec at peak
Latencyp99 under 100 ms — abandonment is latency-sensitivep99 under 1 s is acceptable
Availability99.99% — a dark storefront is lost revenue99.99%, and correctness outranks it
ConsistencyEventual — a 60-second-stale price or stock count is fineStrong — inventory and payment must be exact
Partition behaviorAP — keep serving from cacheCP — refuse rather than oversell
Scalability10x spike, absorbed automatically10x spike, but from a small base

Two more NFRs worth stating because they get forgotten:

  • Security — payment data means PCI scope. Card details never touch our servers; we use a payment provider's tokenized flow. This shrinks the compliance boundary enormously and is worth one sentence.
  • Durability — orders and payments must survive anything. This ranks above availability: refusing an order is recoverable, losing a paid one is not.

3. Core Entities & Key State

StateVolumeConsistencyWhere it lives
Catalog (products, prices, descriptions)10M SKUs, read-dominatedEventualObject store + CDN + cache; source of truth in a database
Inventory counts10M rows, high contention on hot itemsStrongTransactional store, sharded by SKU
Cart1 per active user, ephemeralSession-scopedFast key-value store with TTL
OrdersAppend-only, permanentStrong + durableTransactional store, sharded by user
Sessions / auth1 per active userRead-your-writesToken-based — keep the app tier stateless

The last row matters more than it looks. Sessions in server memory would force sticky routing and break horizontal scaling, so authentication state goes in a signed token the client carries. That one decision is what makes the app tier stateless, which is what makes everything else scale.

4. API & Interfaces

GET  /v1/products?q=...&page=...        -> cacheable, public
GET  /v1/products/{sku}                 -> cacheable, public, high volume
POST /v1/cart/items                     -> session-scoped
POST /v1/orders    Idempotency-Key: ... -> transactional, exactly-once
GET  /v1/orders/{id}                    -> read-your-writes for the owner

Three deliberate choices to narrate:

  • The catalog reads are cacheable GETs with explicit cache headers, so the CDN can serve them without touching us. Making them cacheable is a design decision, not an implementation detail.
  • POST /v1/orders carries an idempotency key. Chapter 1, Lesson 4 — a user who double-clicks or a client that retries after a lost response must not place two orders.
  • Order reads need read-your-writes. The confirmation page immediately after checkout cannot hit a lagging replica and report "no orders found."

5. High-Level Design

(a) The naive core

Fails at roughly one thousandth of the required load. But say why: every catalog read hits the database, the app tier is a single point of failure, and there is no path to more capacity.

(b) Separate the two paths

Narrate the flow of the 3 million reads per second:

"The CDN absorbs images and static assets outright — that's the bulk of the bytes and it never reaches us. Product pages are cached with a short TTL, so the vast majority of the remaining reads are served from cache. Only cache misses reach read replicas, and essentially nothing reaches the primary. Realistically that leaves a few thousand database reads per second, which is ordinary."

That is the whole scaling story, and it is the arithmetic from Lesson 10 doing the work.

(c) Absorb the spike

Only two things are synchronous on the checkout path: reserving inventory and authorizing payment. Everything else — receipts, fulfillment, analytics — goes on a queue. This keeps the critical path to two dependencies, which matters because of Lesson 2's arithmetic: every synchronous dependency multiplies into the checkout path's availability.

(d) The mature picture

Add: multi-AZ for every tier, read replicas per region, inventory sharded by SKU so hot items don't contend with the whole catalog, autoscaling on the stateless tiers, and pre-warmed capacity before a known sale.

6. Deep Dives & Follow-up Questions

"Two users buy the last item at the same moment. What happens?"

This is the correctness core of the problem. Inventory decrement must be atomic and conditional — a single transaction that decrements only if stock is greater than zero, so exactly one of the two succeeds and the other gets a clean out-of-stock error. What I will not do is read stock, decide in application code, then write; that's a race that oversells under exactly the load where it matters most. For very hot SKUs I'd shard the counter into buckets to cut contention, and reserve inventory with a short TTL at cart-checkout so an abandoned payment releases it automatically.

"How is the browse path still correct if it's serving stale data?"

It isn't fully correct, and that's deliberate. A product page may show a price or stock count up to a minute old. The mitigation is that the cached value is never authoritative for a decision that costs money — inventory is re-checked transactionally at checkout, and price is re-validated against the source of truth before payment. So staleness can mislead a user's browsing, which is cheap and recoverable, but it can never cause an oversell or a wrong charge.

"A region goes down during a sale."

Browse degrades gracefully: other regions and the CDN keep serving, possibly staler. Checkout is the harder call — it's CP, so if the transactional store's quorum is unreachable from the surviving regions, checkouts fail rather than risk overselling. I'd rather show "we can't complete your order right now" than sell inventory twice and cancel orders afterwards, which is worse for trust and worse operationally. Meanwhile the store stays browsable, so we lose conversions rather than the entire site.

"Your p99 is fine but customers complain it's slow. Explain."

Two likely causes. First, the tail is skewed toward heavy users — large carts, long order histories, big wishlists — so aggregate p99 hides their much worse experience; I'd segment latency by account size. Second, this is the gray-failure shape from Chapter 1: if we alert on server-side health we can be green while clients time out. I'd measure success rate and latency from the client, and slice by region and device.

"Where does this design break first at 10x?"

Not the app tier — it's stateless and autoscales. The first bottleneck is inventory write contention on popular SKUs, because that's the one place I've deliberately chosen strong consistency and serialized access. That's contention in the Universal Scalability Law sense, and adding machines won't fix it. The fix is sharding the hot counters and shortening the time a reservation is held. The second is cache miss rate during a spike, when traffic arrives for cold SKUs — pre-warming the cache for promoted products before a sale addresses that.

"What would you cut to hit the launch date?"

Multi-region. Single-region with multi-AZ gets 99.99% and covers the realistic failure modes; going global multiplies cost and complexity for a resilience tier we don't yet need. I'd design the data layer so region is a partition dimension from day one so it doesn't require a rewrite later — but I wouldn't build it now.

Two more worked NFR examples

Interviewers often ask you to map NFRs to strategies for a named product. Two canonical answers:

Design Google Maps

A navigation system must identify locations, find optimal routes, and provide turn-by-turn directions. The road network graph is far too large for a single server.

Non-functional requirementStrategies
AvailabilityDivide the road network graph into small graphs (segments) to process user queries · Replicate the segment servers · Load balance requests across different segment servers
ScalabilityPartition large graphs into smaller graphs to ease segment addition · Host graphs on different servers to handle more queries per second

The insight: partitioning serves both NFRs at once. Splitting the graph into replicated segments removes the single point of failure (availability) and lets you add capacity by adding segments (scalability). Segment servers answer route requests for their own region independently.

Design YouTube

A video platform lets users upload, search, stream, and rate videos.

Non-functional requirementStrategies
Less response timeCache at different layers · CDNs · Choose appropriate storage per data type (blob storage for videos, Bigtable for thumbnails) · Serve videos and static content with a lightweight web server such as Lighttpd
ReliabilityData sharding to isolate failures · Replicate critical components · Heartbeat protocol to detect and remove faulty servers

The insight: different data types get different storage. Videos are huge and streamed, so they go to blob storage behind a CDN with caching at ISP and CDN levels. Thumbnails are small, numerous, and randomly accessed, so they go to a wide-column store. Choosing one system for both would be wrong for one of them.

Now do it live

The next section drills these concepts as standalone scenarios, with model answers.

Enjoying the preview?

Create a free account to unlock the rest of this course, the in-browser judge, and live AI mock interviews.

Sign up free to continue