</> 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 Counters, One Till - API Design (With Interviewer Pushback)

Executive Summary: This section designs the system's three API surfaces — the customer surface (versioned menu, client-held cart, server-priced checkout, order status), the kitchen surface (prep queue, READY, and the 86 board), and the admin surface (menus published as immutable versions, never edited in place). We walk order #1042 through the contract end to end, state the price-change-mid-cart rule precisely, and survive three rounds of pushback: why the client sends a delta instead of a resolved ingredient list, why customers poll while only the kitchen display gets a socket, and why PUT /menu is the wrong instinct entirely.

The interviewer says "Walk me through your API," and the trap here is the opposite of the color picker's. That system rewarded ruthless minimalism; this one rewards noticing that three different kinds of people talk to it, and they must not share a door. Maya wants to browse and buy. Marco wants to cook and shout "86 avocado" at a tablet instead of at the room. A manager wants to change Tuesday's prices without rewriting history. One surface per caller, with authorization drawn at the surface — not sprinkled per-endpoint — is the shape of the answer.

Surface One: The Customer

Browsing starts with one read, and its response carries two version numbers — because Section 2's core tension (menu freshness vs cache cheapness) is resolved by two documents on two clocks (D6):

GET /menu?location=downtown
Accept: application/json

200 OK
Cache-Control: public, max-age=300
{
  "menuVersion": 47,
  "availabilityVersion": "a-312",
  "categories": [
    { "id": "protein",
      "ingredients": [
        { "id": "chicken",  "name": "Grilled Chicken", "priceCents": 350,
          "allergens": [] },
        { "id": "avocado",  "name": "Avocado",         "priceCents": 200,
          "allergens": [] }
      ] }
  ],
  "recipes": [
    { "id": "harvest-cobb", "name": "Harvest Cobb", "basePriceCents": 1195,
      "ingredients": [
        { "ref": "romaine",     "required": true  },
        { "ref": "blue-cheese", "required": false },
        { "ref": "chicken",     "required": true, "quantity": 1 }
      ] }
  ]
}

Two shapes in that document do most of the topic's work. Ingredients carry their own prices and allergens — the ingredient is the atom (D1), so the menu is a catalog of atoms plus recipes, which are compositions of references. And every recipe ingredient declares required or default — the single boolean that makes the 86 cascade computable in Section 8 instead of hand-curated.

Notice what the menu document does not carry: availability bits. Those live in the second document of D6's split, with its own read and its own clock:

GET /availability?location=downtown

200 OK
Cache-Control: public, max-age=15
{
  "availabilityVersion": "a-312",
  "eightySixed": []
}

Roughly 1 KB — empty at 12:10, ["avocado"] as a-313 after Marco's tap. The menu caches hard for five minutes; this overlay lives on a 15-second TTL plus push invalidation, which is precisely how the ≤30-second cascade NFR is met without ever busting the 30 KB menu's cache.

The cart is client-held. The server offers a validation call (POST /cart:validate, same body shape as checkout, no side effects) so the app can reprice and grey out 86'd items as Maya edits — but there is no server-side cart resource. Carts are 20-minute scratchpads; persisting them buys sync complexity and earns nothing until checkout, where everything the cart claimed gets re-derived anyway.

Checkout is the endpoint where every rule this topic has stated becomes enforceable. Maya's order #1042, exactly as the client sends it at 12:11:

POST /orders
Authorization: Bearer <customer token>
Idempotency-Key: 9b2f61d4-7c3e-4a1b-9e2d-5f8a0c4d7e21

{
  "location": "downtown",
  "menuVersion": 47,
  "lines": [
    { "recipeRef": "harvest-cobb",
      "delta": {
        "remove":   ["blue-cheese"],
        "add":      ["avocado"],
        "quantity": { "chicken": 2 }
      } }
  ],
  "paymentToken": "psp-tok-91kd"
}

Notice what the client did not send: a price. Not per line, not a total, not a tip-adjusted anything. The server resolves the delta against v47, prices the result, runs the final availability check against the current overlay, authorizes payment with the PSP under that idempotency key — a plain UUID minted once per order attempt; retries resend the same key unchanged — and only then answers:

201 Created
{
  "orderId": 1042,
  "status": "PLACED",
  "totalCents": 1745,
  "lines": [
    { "name": "Harvest Cobb (modified)",
      "priceCents": 1745,
      "breakdown": [
        { "item": "Harvest Cobb base", "cents": 1195 },
        { "item": "Add avocado",       "cents": 200  },
        { "item": "Double chicken",    "cents": 350  }
      ] }
  ],
  "menuVersion": 47,
  "promisedAt": "12:32"
}

$11.95 + $2.00 + $3.50 = $17.45, computed in exactly one place (D3), and promisedAt: 12:32 is not a vibe — it's the queue math from Section 3 (40 salads deep ÷ 120/hour), which Section 8 derives in full. Status thereafter is GET /orders/1042: the client polls every 10 seconds, and a push notification fires on READY.

Maya's checkout sequence: MayaApp POSTs the order to the API, OrderService resolves and prices against v47, checks availability a-312, PSP authorizes $17.45, and MayaApp gets 201 PLACED with pickup at 12:32

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.