</> MAANG.io
system design · 301

Advanced

Master system design interviews with scalable architecture patterns, distributed systems, and real-world design challenges.

0/480 solved 0% complete

Section 4: Three Reads and a Refusal - API Design (with Pushback)

Executive Summary: Section 3's arithmetic said the volume is small and the semantics are hard, and the API inherits exactly that shape. This section designs the whole public contract: one Top-10 endpoint over 93 precomputed lists, one rank endpoint (plus a batch variant) over an hourly snapshot, one category best-seller endpoint, and, just as deliberately, no write endpoint at all. Then it defends the contract through five rounds of pushback: arbitrary windows, exposing raw unit counts, why every response carries asOf, why checkout doesn't call us, and how many category ranks a product page should show. The lesson underneath: an API is as much a list of things you refuse to promise as a list of things you serve.

The interviewer uncaps a marker and slides it across the table: "Okay. What do the clients actually call?"

Section 3 already told you who the callers are and how hard each one pushes: the Top-10 button (2,300/s at sale peak, 93 possible answers), product detail pages wanting a badge (116k/s at sale peak, 40M possible answers), and category best-seller pages (580/s). Three audiences, three read shapes. So you write four lines and stop:

GET  /v1/top-products?scope=overall|dept:{departmentId}&window=1h|4h|24h&limit=10
GET  /v1/products/{productId}/sales-rank
POST /v1/sales-ranks:batchGet                      (≤ 100 productIds)
GET  /v1/categories/{categoryId}/best-sellers?limit=100

You: "Every one of those is a read. There's no POST /purchases, no PUT /scores. Rankings are derived data: they come from order events that already exist in the Orders DB, and I'll come back to why the ranking service never takes a write from anyone."

The Top-10 Button: An Enum, Not a Query

The first endpoint looks like a query, but it's really a lookup. scope is overall or one of the 30 departments; window is exactly 1h, 4h or 24h. That makes 31 × 3 = 93 lists, every one precomputed by the speed path each minute. Anything else, whether window=7h, window=90m or scope=dept:does-not-exist, gets a 400 that names the allowed values.

Here is Priya at 20:05, clicking "Top 10 · last 4 hours" during the evening sale:

GET /v1/top-products?scope=overall&window=4h&limit=10

200 OK
Cache-Control: public, max-age=30
{
  "scope": "overall",
  "window": "4h",
  "asOf": "2026-10-28T20:04:00+05:30",
  "items": [
    { "rank": 1, "productId": "P-10001", "title": "Nova X5 Smartphone",
      "imageUrl": "https://img.clipkart.com/p/P-10001.jpg",
      "price": { "amount": 24999, "currency": "INR" } },
    ...
  ]
}

Three details carry the design. First, Cache-Control: public, max-age=30. Nobody's Top-10 is personal, so all 2,300 sale-peak requests per second collapse onto 93 cache keys. With an origin shield in front of the API, the origin sees about 3 requests a second. The shield and the edge both honour the Age header, so 30 s is the end-to-end cache age, not 30 s per tier. Without one, each of 50 POPs refreshes every key on its own, and the origin sees about 155. The rejected alternative, no cache, recomputes the same 93 answers thousands of times a second. The flip trigger is personalization: once lists vary by shopper segment, the CDN caches per segment, never per user.

Second, limit is capped at 10. The pipeline keeps top-20 candidates per list because the API hydrates title, price and image from the catalog at read time and drops anything out of stock, delisted or restricted. If we allowed limit=20, some responses would come back with 17 items, and we'd be making a promise we can't keep. The extra ten candidates are a filtering buffer. They are not part of the contract.

Third, the response holds no counts. It has a rank and a product, and nothing else. Pushback 2 explains why.

Sequence of a Top-10 request: Priya's browser asks the CDN with origin shield, a miss is forwarded once per 30 s to the Top-10 API, which reads the overall 4h Top-K list (version 20:03, asOf 20:04, 20 candidates), hydrates the 20 ids against the Catalog for stock and listing, and returns the first 10 eligible items with max-age 30, so the browser shows the Top 10 as of 20:04

On a CDN hit, the whole exchange is the first and last arrows: about 10 ms. On a miss, the origin's p99 budget is 50 ms, and that is easy to meet when the "query" is one Redis read plus a batched catalog lookup.

Quick check before moving on:


Sign in to continue reading

The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.

Sign in to read the full lesson

Sign in to MAANG.io

Use your Google or Microsoft account — no password to remember.

Continue with Google Continue with Microsoft

Please accept the terms above to continue.