</> 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: Bytes Go Around, Not Through - API Design (with pushback)

Executive Summary: The contract has four jobs: get a listing and its photos in, find listings near a buyer, let the seller change or close a listing, and let two strangers talk without swapping phone numbers. We walk Maya's posting calls and Daniel's search URL, then defend four choices under pushback: photos bypass the API, exact coordinates go in but only fuzzed ones come out, search uses cursors instead of page numbers, and a new listing returns pending_review, not active.

"Show me the API." Write the whole surface first, one line per endpoint, before arguing any of it.

Job Endpoint
Get an upload slot POST /v1/uploads
Post a listing POST /v1/listings
Read a listing GET /v1/listings/{id}
Edit, drop price, mark sold PATCH /v1/listings/{id}
Renew, remove POST /v1/listings/{id}/renew, DELETE /v1/listings/{id}
Search near me GET /v1/search
Message a seller POST /v1/conversations, POST /v1/conversations/{id}/messages, GET /v1/conversations
Report POST /v1/reports

"Everything is JSON under /v1, behind a bearer token. No request body anywhere carries a sellerId or buyerId. Who you are comes from the verified token and nowhere else, so 'edit someone else's listing' is not a request a client can even write."

Posting: Two Steps, and the Bytes Skip the API

Maya is in Mueller at 7:40 pm on a Sunday, inside the weekly posting peak, with six photos of a Trek road bike. The app first asks for upload slots, one per photo:

POST /v1/uploads
{ "contentType": "image/jpeg", "bytes": 1520000 }

201 Created
{ "uploadId": "up_8c1f…", "uploadUrl": "https://uploads…/raw/…?sig=…",
  "expiresInSec": 900, "maxBytes": 5000000 }

The uploadUrl is presigned: it permits PUTs to exactly one object key in a private quarantine bucket for 15 minutes (reusable until expiry, so retries work), with the content type and the exact declared Content-Length signed in (the maxBytes here is an illustrative cap). A presigned POST policy with a content-length-range is the alternative when size isn't known. The app has already resized each photo to 2,048 px on the long edge, so each PUT is about 1.5 MB, and it goes straight to object storage. Then the listing itself, which is a few kilobytes:

POST /v1/listings
Idempotency-Key: 3b9d0e44-71a2-4c5e-9f08-2d6a1c7e90b3
{ "title": "Trek road bike, 56 cm", "description": "…",
  "priceCents": 45000, "categoryId": 412, "condition": "good",
  "location": { "lat": 30.2985, "lng": -97.7044 },
  "photoUploadIds": ["up_8c1f…", "…5 more"] }

202 Accepted
{ "listingId": "1849302217733120", "status": "pending_review", "version": 1 }

The Idempotency-Key matters on a phone. If the connection drops after the server commits but before the response arrives, the retry with the same key returns the same listingId. Without it, a flaky network posts the bike twice, and identical listings are exactly what Section 9's trust-and-safety pipeline flags as spam.

Posting sequence: the app gets six presigned URLs, PUTs each ~1.5 MB photo straight to object storage, then POSTs the listing with an Idempotency-Key; the API runs sync checks under 200 ms, returns 202 pending_review, sends an outbox event via Kafka to moderation, which approves within p95 20 s, and the API pushes 'listing is live' through APNs or FCM

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.