Mock interview: design an API
Published
This is the real test. A senior “design X’s API” round isn’t a quiz on one topic — it’s a 45-minute integration exam. In one prompt the interviewer watches you balance resource design, reliability, security, performance, and async at once, then pivot when they twist a constraint. The fast way to flunk is to start typing endpoints. This lesson gives you a repeatable framework you can draw, one fully worked example, and three prompts to rehearse live.
There’s no one answer
they grade your structure
so run a checklist
and narrate the why
The senior API-design framework
Run these steps in order, out loud in any whiteboard round. The endpoints fall out of the resource model; the resource model falls out of the questions you ask. Lead with the questions and the rest writes itself.
1 · Clarify Clients (1st vs 3rd-party)? Scale & read/write ratio? Consistency, auth, SLA, latency budget? Pin before drawing.
2 · Resources & URIs Name the nouns, their ownership, the hierarchy. The data model is the design; verbs come second.
3 · Methods + codes Each operation to the right method and precise code: 201 vs 202, 200 vs 204, 409 vs 422.
4 · Writes are safe Money/create POSTs carry an
Idempotency-Key+ dedup store so retries never double-fire.
5 · Collections Cursor pagination, a filter/sort contract, bounded page sizes on every list endpoint.
6 · Cache & concurrency ETag+Cache-Control on reads;
If-Match on mutables to stop lost updates.
7 · Versioning A strategy, a definition of “breaking”, and a stated deprecation path.
8 · Auth + BOLA Authenticate, authorize per-scope, check object-level ownership on every access.
9 · Errors & limits RFC 9457 problem+json, 429 with limit headers,
request IDs into logs/traces.
10 · Async / webhooks Long work returns 202 + status resource; results
via signed, at-least-once webhooks.
11 · 100× trade-offs Name what you’d revisit at scale: hot partitions, caching tiers, sharded cursors, back-pressure.
The arc Diagnose → design → defend. A clear checklist narrated aloud beats brilliant free-association.
Don’t design — diagnose, then design. The first five minutes are requirements and constraints. The endpoints fall out of the resource model; the resource model falls out of the questions you asked.
What separates junior from senior
Interviewers grade three things, not the “right” answer. Here is the rubric they carry in their head.
✓ Truth: the best answer clarifies, covers all five dimensions, and attaches a reason to each choice.
Worked example — a payments service API
Prompt: “Design the API for a payments service — charge a card, refund, list transactions.” A strong answer stays tight: a sentence per step, real endpoints, the why attached to each call. Start from the resource sketch.
Idempotency · the headline Every POST /charges and
POST /refunds
needs an Idempotency-Key; store key→result ~24h. Non-negotiable on a money
path.
Collections ?customer=&limit=&starting_after= — opaque cursor
(stable under inserts), filter by customer/status/created, hard-cap limit at
100.
Cache & concurrency Settled charges get long-lived Cache-Control+
ETag; mutable metadata needs If-Match → 412 on a
stale tag.
Versioning Path /v1 for the coarse boundary + a date-pinned version
header per account (the Stripe model) for fine-grained, non-breaking evolution.
Auth + BOLA OAuth2 client-credentials, scopes like charges:write;
object-level check that this token owns
{id}
— else 404 (not
403, no leak).
Errors, limits, async RFC 9457 problem+json with a trace_id; per-account
token bucket → 429; async capture returns 202 + an HMAC-signed,
at-least-once charge.succeeded webhook.
Why this scores: it clarified first, covered all five dimensions (design, reliability, security, performance, async) without being told to, attached a reason to each choice, and finished with scale trade-offs. At 100×: shard idempotency/charge stores by account, watch hot merchants (per-account caps protect neighbors), move list reads to a replica, put webhooks behind a durable queue with retry/back-off + a dead-letter, and ring-fence the synchronous charge path with circuit breakers around the card network.
🎤 Memory rule: Diagnose, then design, then defend — clarify before drawing, give writes idempotency and reads pagination, check object-level ownership everywhere, and close on what changes at 100×.
Now you — three prompts to rehearse
Run the framework yourself. For each prompt, write your answer from memory (don’t peek), then reveal a senior-approach sketch and compare against your structure — did you hit clarify, resource model, idempotent writes, paginated reads, auth/BOLA, and async where it mattered?
Prompt 1 — URL shortener
"Design the API for a URL shortener — create short links, redirect, analytics."
Clarify: read-dominated (redirects ≫ creates), public reads but authed creates, custom-alias support, latency-critical redirect path.
Resources & writes:
POST /links{url, custom_alias?}→201; make it idempotent byIdempotency-Key(or by hashing the long URL) so retries don’t mint duplicate codes.- Redirect semantics:
GET /{code}
→ 301 for permanent/SEO vs 302 when you must count every hit or may re-point; note 301 is cached aggressively so analytics suffer — a deliberate trade-off.
- Analytics:
GET /links/{code}/stats
with cursor pagination over click events; counts are eventually consistent, aggregated async off a click stream, not on the hot redirect path.
Protect it: rate-limit creates per key (
429), auth + object-level ownership on stats and delete, and cache redirects hard at the edge.At 100×: precompute codes from a key-gen service to avoid write contention; the redirect is a cache lookup, not a DB hit.
Prompt 2 — Hotel / room booking
"Design the API for a hotel/room booking system — search, hold, book, cancel."
Clarify: inventory is scarce and contended — double-booking is the cardinal sin; consistency over availability on the book path.
Hold as a first-class resource:
POST /holdsreserves a room with a short TTL (e.g. 10 min) →201with expiry; decrements available inventory atomically. This is the senior move — separate hold from book.Booking:
POST /bookings{hold_id} →201,idempotent viaIdempotency-Keyso a retried payment doesn’t book twice;409if the hold expired.Concurrency: optimistic concurrency (version/
If-Match) on the inventory row so two simultaneous holds can’t both win; loser gets412/409.Search & cancel:
GET /rooms?dates=&guests=&limit=cursor-paginated and cacheable;POST /bookings/{id}/cancel
enforcing acancellation policy (refund tier by time-to-checkin) and releasing inventory.
At 100×: expire stale holds via a sweeper/queue, partition inventory by property/date, and guard against thundering herds on popular dates.
Prompt 3 — File storage (Dropbox-like)
"Design the API for a file storage service like Dropbox — upload large files, share, list, versions."
Clarify: files can be huge (GBs), uploads must survive flaky networks, sharing needs fine-grained access; bytes go to object storage, metadata to the API.
Large uploads: resumable/multipart —
POST /uploadsopens an upload session →202+ session URL; client PUTs chunks, thenPOST /uploads/{id}/complete
. Decouples bytes from metadata and lets retries resume.
Content addressing: identify blobs by content hash so identical files dedup and uploads are naturally idempotent;
If-Match/ETag on metadata mutations.Sharing: signed URLs (time-boxed, scoped) for direct download/upload to object storage, plus permission resources for collaborator access; object-level checks throughout (BOLA).
- List & versions:
GET /folders/{id}/files?limit=&cursor=
cursor-paginated; versions as a sub-resource
GET /files/{id}/versions
with restore.
At 100×: push transfer to a CDN/object store via signed URLs (the API never proxies bytes), shard metadata, and fan out share/notification events through webhooks.
I’m your interviewer — let’s run it for real. Pick any of the three prompts above, type your full answer, and paste it into the chat. I’ll grade it the way a senior panel would — score your structure, breadth, and judgment — and then fire the follow-ups they’d fire: “what happens on a retry?”, “how do you stop user A reading user B’s resource?”, “what breaks at 100×?”. Want a fourth, harder prompt? Just ask.
Primary source (read this next)
📖
Designing Web APIs (O’Reilly)
— the single best end-to-end treatment of the decisions this framework walks. Then study Stripe’s API as the worked exemplar: it is the cleanest public model of idempotency keys, cursor pagination, dated versioning, signed webhooks, and problem-style errors all in one product — read its reference like a design textbook.
Interview — pick your bar
Answer out loud in ~60s, then reveal. Core = recall · Senior = trade-offs & failure modes · Staff = synthesis under ambiguity · System Design = open design round (a different axis, not a harder level).
What is a "design X's API" round actually testing?
Not whether you produce the answer — there isn't one. It grades structure (did you clarify before designing?), breadth (writes got idempotency, reads got pagination, you named object-level auth), and judgment (you named a trade-off and what changes at 100×). It's a 45-minute integration exam over every prior topic at once, not a quiz on one.
What should you do first in a design round, before any endpoint?
Clarify requirements and constraints — clients, scale, read/write ratio, consistency needs, auth model, SLAs. Diagnose, then design. The resource model falls out of the questions you ask; the endpoints fall out of the resource model. Drawing URIs before clarifying is the classic junior tell.
What's the winning arc of a design answer in one phrase?
Diagnose, design, defend. Clarify constraints first, walk the resource model and endpoints with a reason attached to each choice, then close with a trade-off and what changes at scale. A candidate who narrates a clear checklist and the why beats one who free-associates brilliant fragments.
For any "design X" prompt, what two things must every list endpoint have?
Pagination (cursor-based at scale, with a bounded/hard-capped page size) and a filter/sort contract. An unbounded list is a latency and memory landmine. Reaching for these on every collection without being prompted is a fast breadth signal — the mirror of reaching for idempotency on every unsafe write.
What's the single biggest mistake candidates make in this round?
Starting to type endpoints. Don't design — diagnose, then design. The first five minutes are requirements and constraints. The endpoints fall out of the resource model; the resource model falls out of the questions you asked. Leading with verbs before you know the clients, scale, and consistency needs signals junior.
Walk the 11-step framework from memory.
(1) Clarify requirements/constraints; (2) resource model & URIs; (3) endpoints — methods + status codes; (4) idempotency on unsafe writes; (5) collections — pagination/filter/sort; (6) caching & concurrency (ETag/If-Match); (7) versioning & evolution; (8) authN/authZ + object-level (BOLA); (9) errors, rate limiting, observability; (10) async/webhooks where needed; (11) trade-offs & "what changes at 100×." Name the step, make the one decision that matters, signal you know the rest is there.
How do you communicate a trade-off so it reads as "senior"?
State the option, pick a side, attach the reason, and name when you'd decide differently: "That's a deliberate trade-off because X; at 100× I'd revisit it because Y." Defaulting silently (offset pagination, no idempotency) reads junior; choosing and defending reads senior. Other tells: "it's at-least-once, so the consumer must be idempotent," "I care about p99, not the mean," "exactly-once effect, not exactly-once delivery."
Cursor vs offset pagination — defend your default.
Default to cursor (keyset) at any real scale: deep pages stay cheap (no OFFSET N scan) and stable under concurrent inserts/deletes — offset skips or repeats rows when the set shifts. Offset is fine only for small, slow-changing, jump-to-page-N UIs. Always cap page size. The senior tell is naming stability under writes, not just performance.
The interviewer twists a constraint mid-answer ("now there are 10M third-party clients"). How do you respond?
Don't restart. Re-anchor to the framework and say what changes: untrusted third parties tighten authZ (object-level checks, scoped tokens), force strict versioning + deprecation discipline, demand per-tenant rate limits and abuse controls, and push you toward HATEOAS/discoverability so you can evolve URLs without redeploying their code. Naming the deltas — rather than re-drawing — is the signal that you understand which decisions are constraint-sensitive.
Consistency vs availability — how does CAP show up in API design?
Decide per resource/operation, not globally. Money, inventory, and bookings need strong consistency on the write path — refuse or fail (409/503) rather than risk a double-charge or oversell. Feeds, counts, search, and history can be eventually consistent and stay available. Make it visible in the contract: an explicit status/pending state, a "may be stale" note, read-your-writes only where it's promised. The senior move is naming which path is which and why.
"What changes at 100×?" — how do you answer this generically?
Walk the bottlenecks in order: hot partitions / keys (a whale tenant or merchant — protect with per-entity caps), reads move to replicas + caching tiers + CDN, writes get sharded (and cursors must stay stable across shards), async work goes behind durable queues with back-pressure and DLQs, and the synchronous critical path gets kept narrow and wrapped in circuit breakers. Close with what you'd measure (p99 per route) to know when to act. This step is the headline senior signal.
- Clarify requirements & constraints: clients (1st vs 3rd-party), scale & read/write ratio, consistency, auth, SLA — pin before drawing.
- Model resources & URIs: name the nouns, ownership, shallow hierarchy — the data model is the design.
- Map methods + precise status codes: 201 vs 202, 200 vs 204, 409 vs 422.
- Idempotency on unsafe writes:
Idempotency-Key+ dedup store on money/createPOSTs. - Collections: cursor pagination, a filter/sort contract, bounded page sizes on every list.
- Caching & concurrency:
ETag+Cache-Controlon reads,If-Matchon mutables to stop lost updates. - Versioning & evolution + authN/authZ with object-level (BOLA) checks on every access.
- Errors, rate limits, observability: RFC 9457 problem+json,
429+ limit headers, request/trace IDs. - Async / webhooks where it matters:
202+ status resource, signed at-least-once webhooks. - Trade-offs & "what changes at 100×": hot partitions, caching tiers, replicas, sharded cursors, queues + DLQ, back-pressure.
Design the API for a payments service — charge a card, refund, list transactions.
- A strong answer covers: clarify first — untrusted merchant callers, write-heavy money path / read-heavy history, charges strongly consistent & exactly-once from the caller's view, server-to-server auth, "never double-charge."
- Resources:
Charge,Refund(sub-resource of a charge),Customer,PaymentMethod; money as integer minor units + currency, never floats. - Endpoints:
POST /v1/charges→201sync /202async capture;POST /v1/charges/{id}/refunds→201(422over captured total,409if fully refunded);GET /v1/charges?customer=&limit=cursor-paginated. - Idempotency — the headline:
Idempotency-Keyon every charge/refund, key→result stored ~24h; a replay returns the original response. - Auth + object-level: OAuth2 client-credentials, scopes like
charges:write; verify this account owns{id}or return404(don't leak existence); RFC 9457 errors; per-account429with limit headers. - Async + at 100×: async capture →
202+ HMAC-signed at-least-oncecharge.succeededwebhook (consumer dedups); shard idempotency/charge stores by account, watch hot merchants, replica for list reads, queue + DLQ behind webhooks, circuit-break the card-network path.
Design the API for a hotel / room booking system — search, hold, book, cancel.
- A strong answer covers: clarify — inventory is scarce and contended; double-booking is the cardinal sin; consistency over availability on the book path.
- Hold as a first-class resource:
POST /holdsreserves a room with a short TTL (e.g. 10 min) →201+ expiry, decrementing inventory atomically. Separating hold from book is the senior move. - Book:
POST /bookings{hold_id} →201, idempotent viaIdempotency-Keyso a retried payment doesn't book twice;409if the hold expired. - Concurrency: optimistic concurrency (
If-Match/version) on the inventory row so two simultaneous holds can't both win; loser gets412/409. - Search/cancel:
GET /rooms?dates=&guests=&limit=cursor-paginated, cacheable;POST /bookings/{id}/cancelenforces a refund-by-time policy and releases inventory. - At 100×: sweep stale holds via a queue, partition inventory by property/date, guard thundering herds on hot dates; object-level checks so callers only touch their own bookings.
📄 Keep handy:
Senior API design interview framework
◂ Prev · End of the track
These notes reflect my current understanding and are updated as I learn, build, and discover better explanations.