Blueprint
Fundamentals

API design

Five minutes of the interview is literally 'define the API' — and two follow-ups (pagination and idempotency) are where list endpoints and payment endpoints go to die.

01

What it is

The API is the contract layer: how clients invoke your system. Three main styles. REST is resource-oriented HTTP — nouns plus GET/POST/PUT/PATCH/DELETE, JSON, stateless, cacheable. gRPC is an RPC framework on HTTP/2 and Protobuf — binary, typed contracts generated into client code, streaming and deadlines built in. GraphQL is a single endpoint where the client specifies exactly the fields it wants.

In the interview this is an explicit step with about five minutes budgeted, and the expected move is boring: default to REST unless you can justify otherwise. The real scoring happens in two deep-dive hooks that hang off any API — pagination the moment you have a list endpoint, and idempotency the moment an endpoint moves money or creates anything on retry.

Say this out loud

I'll define a REST API — it's cacheable, universal and debuggable — with cursor pagination on the list endpoints and an idempotency key on anything that writes money, and I'd switch to gRPC only for internal service-to-service calls.

02

The picture

Client POST /chargeskey: abc-123 charges1 row key storeabc-123 → resp attempt 1 retry, same key insert once replay cached 200
the client retries a timed-out payment with the same idempotency key. the server recognises the key, skips the second charge, and replays the first response — one row, two 200s.
03

The variants and when to pick each

REST outside, gRPC inside

REST is my public default: HTTP semantics give you CDN and browser caching for free, every client on earth speaks it, and you can debug it with curl. The cost is verbosity — JSON payloads, over- and under-fetching, and N+1 round trips for nested data.

Internally, between my own services, gRPC wins: Protobuf payloads are 5–10× smaller, serialisation is correspondingly faster, clients are code-generated and typed, and deadlines propagate through the call chain. Its weaknesses — no browser support without a proxy, not human-readable, no HTTP caching — don't matter when both ends are mine. "REST at the edge, gRPC inside" is the one-line answer interviewers want.

Trade-offREST pays payload size and round trips for universality and cacheability; gRPC pays debuggability and browser reach for speed and typed contracts.

GraphQL — the right answer rarely, so know when

GraphQL solves over-fetching and lets frontend teams iterate without backend changes — the fit is many diverse clients reading complex nested data, which is exactly Facebook's origin story for it. Mobile apps with dozens of screens each wanting a different slice of the same graph is the honest use case.

The costs are real: caching gets hard because everything is a POST to one endpoint, query depth and cost are unbounded unless you add cost limits, and naive resolvers hit the N+1 problem (fixed with DataLoader-style batching). Name it, scope it, and decline it for a simple CRUD design — reaching for GraphQL on a URL shortener signals résumé-driven design.

Trade-offThe frontend's flexibility is bought with server complexity, lost HTTP caching, and a query engine you must now defend against expensive queries.

Pagination: cursor beats offset, and say why

Offset pagination (?page=3&limit=20) is simple and lets users jump to page N, but it has two failure modes: the database scans and discards offset rows, so page 50,000 costs a million-row scan, and inserts mid-scroll shift everything — users see duplicates or miss items, the classic feed bug.

Cursor pagination returns an opaque token encoding the last-seen sort key (say created_at,id on an indexed column), and the next query is an O(log n) seek from that point. Stable under inserts, constant cost at any depth — the answer for any infinite-scroll feed. The give-up: no random page access and the cursor must encode the sort order, which gets interesting when the feed is ranked by relevance rather than time.

Trade-offCursors trade "jump to page 7" for stability and O(log n) at any depth — the right trade everywhere except admin tables.

Idempotency keys — because retries are everywhere

Same request applied twice must produce the same result once. GET, PUT and DELETE are idempotent by definition; POST is not — and POST is what charges cards. The mechanism: the client generates a UUID Idempotency-Key per logical operation, the server stores key → response (Stripe keeps them 24 h) and replays the stored response on any retry instead of re-executing.

This matters because retries are unavoidable — client timeouts, at-least-once queue delivery, retrying load balancers. A timeout is not a failure: the charge may have succeeded and the response died on the way back. Without the key, every retry is a potential double-charge. I also mention the hygiene items here: version with /v1/, authenticate with JWTs for users and API keys for services, and never accept user_id in the request body — derive it from the token, or any user can act as any other.

Trade-offA key store with TTL and one extra lookup per write buys you safe retries — the cheapest insurance in the whole design, so there's no excuse to skip it on money paths.
04

Numbers worth memorising

Protobuf vs JSON payload~5–10× smaller, similar factor on serialisation
Offset pagination at offset 1Mscans ~1M rows; cursor seek is O(log n)
Idempotency key retention24 h (Stripe's number)
Typical page size20–100 items, hard cap at 100
API step time budget~5 min — 3–5 endpoints, no gold-plating
05

Where the interviewer pushes

The push on any payment design: "the client timed out on 'charge card' and retried — did we double-charge?" Walk it precisely: the first request may have succeeded with the response lost in transit, the retry carries the same idempotency key, the server finds the stored key and replays the recorded response without touching the card again. If the first request is still in flight when the retry lands, the key row acts as a lock — second request waits or gets a 409. Naming the in-flight race is what separates a memorised answer from an understood one.

The push on any feed: "users see duplicate posts while scrolling — why?" Because offset pagination shifts under inserts: a new post at the top pushes everything down one, and page 2 re-serves the last item of page 1. Fix is a cursor keyed on (created_at, id). Then comes the harder variant — "paginate a relevance-ranked feed" — where the cursor must encode the sort score or you snapshot the ranking per session, because relevance can change between pages.

Shows up in

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.