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.
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