RESTful APIs
Design endpoints that fall out of your data model, know which ones a caller can retry blindly, and price the round trips REST costs a mobile screen.
REST is the default API style in a design interview, so the interviewer is not checking whether you know it. They are checking whether your endpoints follow from your data model, and which of them are safe to retry.
What REST constrains
An API (Application Programming Interface) is the contract between two pieces of software: what a caller may ask for and what comes back. REST (Representational State Transfer), defined by Roy Fielding in 2000, is an architectural style for that contract — constraints, not a protocol.
Client-server splits interface from storage and logic, so one backend serves web, mobile and partner clients. Stateless means each request carries what the server needs, so any machine serves any request and a dead instance costs only the calls in flight — see stateful versus stateless services. Cacheable means a response declares its own reusability: an ETag returned as If-None-Match costs a 304 and no body, and a CDN honours the same headers a browser does. Uniform interface does the work: URIs name resources, JSON representations carry them, and method, status code and Content-Type make each message self-describing. HATEOAS is its rarely-implemented fourth part. Layered system hides an intermediary such as an API gateway; code-on-demand is optional.
Endpoints follow from the data model
RESTful in practice means HTTP as the protocol, URIs as resources, methods as actions, JSON representations and status codes: GET /users lists, POST /users creates, GET /users/123 reads, PUT replaces, PATCH updates part, DELETE removes. That list is generated, not memorised — a path segment names a collection or one member, and the method says what is done to it.
Which methods are safe to retry
GET, HEAD and OPTIONS are safe: no side effects. PUT and DELETE are idempotent — five sends leave the same state as one, because each names an end state rather than a delta. POST is neither, and PATCH depends on the body: {"status": "shipped"} is idempotent, {"op": "increment", "by": 1} is not. The detail is in HTTP.
Retries are not optional. A gateway with a 2-second timeout fronts POST /payments at 1,000,000 requests/day: 1,000,000 ÷ 86,400 ≈ 12 QPS average, a few times that at peak, over a link with ~200 ms round trips. Assume 0.1% of calls end ambiguously: 1,000,000 × 0.001 = 1,000 a day. The caller cannot tell a lost request from a lost response, so it retries, and every retry of a committed request is a second charge on a real card.
At-least-once delivery produces duplicates, which make idempotency a requirement: the client sends a key as Idempotency-Key, and the server stores it with the first result and replays that rather than charging again. At-most-once trades those 1,000 double charges for an unknown number of dropped payments.
Status codes are instructions to that machinery. A 4xx other than 429 and 408 means the request is wrong and repeating it reproduces the failure; 5xx, 429 and 408 mean try again later, and 429 should carry Retry-After — see rate limiting. An error inside a 200 breaks all of that: clients, gateways and load balancers read the status line, never the body.
The cost of the style: round trips
An order screen needs the user, their last 20 orders, and each order's items. Resource-per-call gives GET /users/42 and GET /users/42/orders, independent because id 42 is known, so one wave covers both; the 20 calls to /orders/{id}/items wait on the order ids. Over HTTP/1.1 the six-connection-per-host cap makes those ceil(20 ÷ 6) = 4 waves, so 1 + 4 = 5 round trips at ~200 ms ≈ 1,000 ms against a 400 ms p99 target. Over HTTP/2 they multiplex onto one connection and collapse into roughly one wave, still 2 round trips ≈ 400 ms: the whole budget on network, nothing left for TLS on a cold connection or for the server.
Two fixes, both trading purity for round trips. Let the caller ask for more per request: GET /users/42/orders?expand=items&limit=20 rides in the same wave as the user fetch, one round trip, ~200 ms. Or compose server-side in a gateway or backend-for-frontend, where a hop costs 0.5 ms instead of 200 and 20 parallel internal calls add about a millisecond.
The same shape is why REST cannot push: anything live is polled, and 100,000 clients polling every 5 seconds is 100,000 ÷ 5 = 20,000 QPS of mostly-304 traffic saying nothing changed — the argument for WebSockets.
Chattiness bites server-side too. Assume GET /users/{id}/orders takes 8,600,000 requests/day: 8,600,000 ÷ 86,400 ≈ 100 QPS average, roughly 400 QPS at a 4× peak. If it loads 20 orders and then queries items one order at a time, that is 1 + 20 = 21 queries per request: 400 × 21 = 8,400 queries/second against the ~5,000 simple QPS a commodity Postgres box handles. The instinct is to shard; one batched WHERE order_id IN (…) makes it 400 × 2 = 800 QPS instead.
In an interview
What is tested is whether your endpoints fall out of your data model and what happens when one is retried: name the resources, then the methods, then the status codes.
Usable phrasing: "The resources are users and orders. POST /orders creates one and returns 201 with a Location header; GET /users/42/orders?limit=20 reads the list by cursor. POST is not idempotent and the gateway retries on timeout, so the client sends an Idempotency-Key stored with the first result."
The mistake that loses points is answering "design the API" with verbs in paths — POST /createOrder, GET /getOrderList — because the design then came from screens, and the list grows one entry per screen forever. Second is calling a retry policy done without naming the idempotency key.
Check yourself
1. A mobile client in Mumbai calls your API in US East. The screen needs a user, their 20 orders, and each order's items, p99 target 400 ms. How many round trips can you afford, and what does that rule out?
At ~200 ms a round trip, 400 ms buys two, fewer on a cold connection that also pays for TLS. Resource-per-call is out either way: 5 waves ≈ 1,000 ms on HTTP/1.1, and on HTTP/2, where the 20 item calls overlap into one wave, 2 round trips ≈ 400 ms still eat the budget before the server works. What fits is one composite read — an
expandparameter, or a backend-for-frontend fanning out at 0.5 ms a hop — for ~200 ms, buying 800 ms at the cost of a reusable endpoint.
2. Your gateway retries any request unanswered after 2 seconds. POST /payments takes 1M requests/day and roughly 0.1% end ambiguously. What is that policy costing, and do you turn it off?
1,000,000 × 0.001 = 1,000ambiguous outcomes a day, each retried, and each retry of a committed charge is a duplicate: up to 1,000 double charges a day from correct client behaviour. Do not turn it off, because at-most-once drops real payments and you cannot tell which. Keep at-least-once and make the endpoint idempotent: anIdempotency-Keystored against the first response, with a unique constraint so two concurrent retries cannot both win.
3. GET /users/{id}/orders takes 8.6M requests/day — ~100 QPS average, ~400 QPS at a 4× peak — and issues one query per returned order for its 20 items. Do you shard?
No.
400 × (1 + 20) = 8,400queries/second is above the ~5,000 simple QPS one commodity Postgres box handles, but the load is an artefact of the query pattern, not the traffic. Batching the item lookup into oneIN (…)query makes it400 × 2 = 800QPS. Shard when the workload genuinely exceeds one box; sharding to survive an N+1 buys permanent cost to avoid a one-line fix.