Designing the API surface between services deliberately — REST vs gRPC vs async, versioning strategy, idempotency keys, and pagination — as a first-class HLD interview deep-dive, not an afterthought.
Published September 23, 2026
After deciding where the service boundaries are (Domain Decomposition), an HLD interview often expects you to go one level deeper on at least one boundary: what does the actual API contract between two services look like? This is a common deep-dive target precisely because it's where vague hand-waving ("they talk over REST") gets exposed.
REST (JSON over HTTP): human-readable, widely tooled, higher per-call overhead,
best for public/external APIs and browser clients
gRPC (protobuf over HTTP/2): binary, strongly-typed contracts, lower latency/overhead,
best for internal service-to-service calls at scale
Async messaging (a queue): fully decoupled, sender doesn't wait for a response,
best when the caller doesn't need an immediate result
(see Message Queue System / event-driven patterns)
The interview-relevant answer isn't picking one universally — it's matching the choice to the actual call: a public-facing API serving browser clients reasonably defaults to REST/JSON for tooling and readability; high-volume internal service-to-service calls where latency matters benefit from gRPC's lower overhead and strict schema; anything where the caller shouldn't block waiting for the result belongs on a queue, not a synchronous call at all.
URL versioning: /api/v1/orders, /api/v2/orders — explicit, simple, most common
Header versioning: Accept: application/vnd.api.v2+json — cleaner URLs, less visible
Any API contract that will be called by more than one consumer needs an explicit versioning strategy from day one — not because v2 is needed immediately, but because retrofitting versioning onto an API with existing callers (who can't all upgrade simultaneously) is far harder than designing for it upfront. URL versioning is the most common, most interview-safe default: simple, visible, easy for callers to reason about.
POST /api/v1/payments
Idempotency-Key: client-generated-uuid-abc123
— if this exact request (same Idempotency-Key) is received again (e.g. due to a client
retry after a timeout), the server returns the ORIGINAL response instead of processing
the payment a second time
Any API that isn't naturally idempotent (a POST creating something, especially anything involving money — see Payment — Idempotency Implementation) needs an explicit idempotency mechanism, because network failures mean the client genuinely cannot always tell whether a request succeeded or just failed to return a response — a client-generated idempotency key, checked server-side before processing, is the standard answer, letting safe retries happen without risking a duplicate charge or duplicate order.
Offset-based: GET /orders?offset=100&limit=20
— simple, but breaks under concurrent writes (items shift between pages)
Cursor-based: GET /orders?after=order_id_xyz&limit=20
— stable under concurrent writes, standard for any large or actively-changing dataset
Offset-based pagination is simple to reason about but has a real correctness problem at scale: if items are inserted or deleted while a client is paging through results, offset-based pages can skip or duplicate items. Cursor-based pagination (using a stable reference point — typically the last-seen item's ID or timestamp — rather than a numeric offset) avoids this and is the standard choice for any list endpoint expected to be large or under active write load.
Q: When is offset-based pagination actually fine despite its weakness? A: For small, relatively static datasets, or internal admin tooling where occasional skip/duplicate under concurrent writes is a minor, tolerable inconvenience rather than a correctness-critical issue — the cursor-based complexity isn't always worth paying for every list endpoint.
Q: How does API versioning interact with backward compatibility more generally? A: Versioning is the escape hatch for BREAKING changes; many changes (adding a new optional field, adding a new endpoint) don't need a new version at all if done in a backward-compatible way — over-relying on versioning for every change fragments the API into too many simultaneously-supported versions to maintain.
Q: Does an idempotency key need to be stored forever? A: No — typically stored with a bounded retention window (e.g. 24 hours) matching the realistic window in which a client might retry after a failure; after that window, the same key can be safely treated as a new request, keeping the idempotency-key store from growing unbounded.
Q: How would you decide between gRPC and REST for a NEW internal service, given the interoperability cost of switching later? A: Consider the team's existing tooling/expertise, the actual latency sensitivity of the calls involved, and whether the service might ever need to be called from outside the internal network (gRPC is less browser-friendly) — defaulting to REST unless there's a concrete, stated latency or throughput reason for gRPC is a reasonable, defensible interview answer.