Fundamentals
API Design
REST vs. RPC vs. GraphQL, idempotency keys, cursor pagination, and versioning without breaking existing clients.
The API is the one part of the design everyone in the room can actually read without a diagram. Interviewers pay attention to it disproportionately for exactly that reason — a sloppy endpoint shape is visible to everyone, not just to whoever's deep-diving the database.
REST vs. RPC vs. GraphQL
| REST | RPC | GraphQL | |
|---|---|---|---|
| Shape | Resources + HTTP verbs | Named function calls | One endpoint, client-specified query |
| Good fit | CRUD-shaped domains | Internal service-to-service calls | Many clients with different data needs |
| Cost | Verb/resource mismatches get awkward (POST /users/123/deactivate) | No shared convention for "what does 404 mean here" | A slow nested query can hide N+1 database calls behind one request |
None of these is universally correct. A reasonable default: REST for a public-facing CRUD API, RPC for internal service calls where you control both sides, GraphQL only when you actually have several client types with genuinely different data needs — reaching for it by default adds a query planner's worth of complexity you may not need.
Idempotency
A POST /payments call that times out on the client side, then gets
retried, must not charge the customer twice. The standard fix is an
idempotency key: the client generates one per logical operation and sends
it in a header; the server stores which keys it has already processed and
returns the original result on a repeat, instead of re-executing.
Client
POST /payments, Idempotency-Key: abc123
API layer
checks abc123 against processed keys
Not seen before
processes charge, stores result under abc123
Client retries (timeout)
same key abc123
API layer
returns the stored result — no second charge
Pagination: cursor vs. offset
GET /posts?offset=100000&limit=20 looks harmless until the table has tens
of millions of rows — the database still has to scan and discard the first
100,000 rows before it can return the next 20. A cursor (GET /posts?after=post_98213&limit=20) carries a pointer to the last item seen
instead of a row count, so the query becomes "give me 20 rows after this
one" — a lookup, not a scan. The cost: a cursor can't jump to "page 47"
directly, which offset-based pagination can. Most feeds and infinite-scroll
UIs don't actually need that, which is why cursor pagination is the more
common real-world choice at scale.
Versioning
Public APIs change, and existing clients can't be forced to update on your
schedule. Two common approaches: a version segment in the URL
(/v2/orders) or a version header (Stripe-Version: 2024-06-20, the
pattern Stripe's public API actually uses). URL versioning is more visible
and cacheable; header versioning keeps URLs stable and is easier to default
silently for clients that don't specify one. Either is defensible — what's
not defensible in an interview is not having thought about it at all, since
"how do you change this without breaking existing clients" is a near-certain
follow-up on any public-facing API design.