System design interview guide
API Design System Design Interview: Designing a REST, GraphQL, or gRPC API That Stays Backward Compatible, Idempotent, and Observable at Scale
An API is the one part of a system you cannot refactor freely, because every decision you make becomes a contract that other people write code against and then stop touching. A database schema is private and you can migrate it on your own schedule; a public API surface is frozen the moment the first external client integrates, and every field name, status code, error shape, and pagination style has to keep working for years. That is what makes API design a distinct discipline from the internal architecture behind it. The interesting questions are almost never about raw throughput. They are about how you model resources so they still make sense in three years, how you evolve the contract without breaking existing callers, how you make a retried request safe to run twice, and how you tell a client the difference between bad input, insufficient permission, and a genuine server fault. Any latency or throughput figures below are industry-typical ranges for illustration, not measured internal numbers from any specific vendor.
API design is the practice of defining the contract between a service and its callers: the resources it exposes, the operations allowed on them, the shape of requests and responses, the error format, and the rules for authentication, versioning, and evolution. The three dominant styles are REST over HTTP (resource-oriented, cacheable, ubiquitous), GraphQL (a single endpoint with a typed query language that lets clients ask for exactly the fields they need), and gRPC (contract-first binary RPC over HTTP/2, fast and strongly typed, ideal for internal service-to-service calls). Most of the hard problems are the same regardless of style. You have to model resources and name them consistently, choose HTTP methods and status codes that mean what they say, decide how to version without breaking existing clients, paginate large collections without offset drift, make write operations idempotent so retries are safe, return machine-readable errors in a standard format such as RFC 7807 problem+json, authenticate and authorize callers, and defend the service with rate limits and quotas. Good API design leans hard toward backward compatibility, predictability, and least surprise, because the cost of a breaking change is paid by every integrator at once and cannot be undone unilaterally.
Where it shows up
Asked broadly, because almost every backend and platform team owns an API, and it appears at every level from junior (design the endpoints for a to-do app) to staff (design a payments or public platform API with versioning, idempotency, and deprecation policy). It shows up as a standalone question (design a REST API for some product) and as a component inside larger system design rounds, since any design with a client tier, a public platform, or service-to-service communication has to specify its interface. Companies with large public API surfaces (Stripe, Twilio, GitHub, Google Cloud, AWS, Shopify) weight it heavily, and platform, developer-experience, and API-gateway teams treat it as a core competency rather than a warm-up.
Why this question is asked
Interviewers use it because it separates people who can wire up endpoints from people who understand that an API is a long-lived contract with real users on the other side. It cannot be answered by naming a framework. You have to reason about evolution: what happens to existing clients when you add a field, remove one, or change a meaning; how a client safely retries a payment without charging twice; what a caller does when it hits a rate limit; how a mobile app pinned to version 1 keeps working while you ship version 2. It rewards judgment over trivia, because most choices are trade-offs with no universally correct answer (REST versus GraphQL, offset versus cursor pagination, URI versus header versioning, sync versus async for slow operations), and a strong candidate defends a choice by naming what it costs. It also surfaces empathy for the consumer: clear errors, consistent naming, good defaults, and honest deprecation are what make an API usable, and interviewers can tell within a few minutes whether someone has ever been on the receiving end of a badly designed one.
Requirements
Always clarify these in the first 5 minutes of the interview. Do not start drawing boxes until both lists are agreed.
Functional requirements
- Expose resources and operations through a consistent interface (REST endpoints, a GraphQL schema, or gRPC service definitions) that maps cleanly to the domain
- Support create, read, update, delete, and list operations with correct semantics, including partial updates and conditional writes
- Provide pagination, filtering, and sorting on collection endpoints so clients can page through large result sets predictably
- Make write operations idempotent through idempotency keys or conditional requests, so a retried request never duplicates an effect
- Return errors in a single machine-readable format (for example RFC 7807 problem+json) with a stable error code, a human-readable message, and enough context to act on
- Authenticate and authorize every request (API keys, OAuth2 bearer tokens, or signed requests) and scope what each caller can do
- Support long-running operations through an asynchronous pattern (return an operation handle, let the client poll or receive a webhook) rather than blocking the request
Non-functional requirements
- Backward compatibility: additive changes must never break existing clients, and breaking changes must be gated behind an explicit new version with a deprecation timeline
- Predictable performance: bounded response sizes (enforced pagination), consistent latency, and no unbounded queries that let one caller degrade the service
- Protection under load: per-caller rate limits and quotas with clear headers and a retriable response, so one client cannot starve the rest
- Security: transport encryption everywhere, no secrets in URLs, least-privilege scopes, and input validation on every field to reject malformed or hostile payloads
- Observability: every request carries a correlation or request ID that appears in logs and error responses, so a caller and an operator can trace the same request
- Cacheability where it applies: safe, cacheable reads with ETags and Cache-Control so clients and intermediaries can avoid redundant work
- Discoverability and self-description: a published, versioned schema (OpenAPI for REST, the GraphQL schema, or protobuf definitions) that stays in sync with the running service
Back-of-envelope scale estimates
Show your math. Pulling numbers from thin air signals you have not thought about the load.
Typical read latency budget
tens of milliseconds server-side (typical)
A well-designed read endpoint that hits a cache or an indexed lookup should return within tens of milliseconds of server processing before network transit. This is a design target for a healthy API, not a guarantee, and it drives the decision to paginate and to cache rather than returning unbounded collections.
Default page size
20 to 100 items, hard-capped
Collection endpoints need a sane default (commonly 20 to 50) and a hard maximum (commonly 100 to 1000) so a single request cannot pull an entire table. The cap is what keeps response size and latency bounded regardless of how a client sets the limit parameter.
Idempotency key retention
about 24 hours (typical)
An idempotency key must be remembered long enough to cover realistic client retry windows, including retries after timeouts and brief outages. A retention window on the order of 24 hours is a common industry choice (Stripe uses 24 hours) balancing safety against storage cost.
Rate limit granularity
per API key or per token, per window
Limits are enforced per authenticated caller (API key, client id, or user) over a rolling or fixed window, often with separate read and write buckets. Enforcing per caller rather than globally is what isolates a noisy client from everyone else.
Deprecation window for a version
6 to 24 months (typical)
Once a version or field is deprecated, clients need time to migrate. Public API providers commonly give 6 to 24 months with advance notice, sunset headers, and email warnings, because forcing a faster cutover breaks integrations that are not actively maintained.
Max request/response body size
single-digit MB, enforced (typical)
APIs cap body size (commonly a few megabytes) to protect memory and prevent abuse, pushing large uploads to presigned object-storage URLs or chunked/multipart flows instead of inline payloads.
High-level architecture
A client sends a request that first reaches an edge tier, usually a load balancer in front of an API gateway. The gateway is where the cross-cutting concerns of the API contract live: it terminates TLS, authenticates the caller (validating an API key, a bearer token, or a request signature), enforces rate limits and quotas per caller, and routes the request to the service that owns the resource. Putting authentication, throttling, and routing at the gateway keeps those policies consistent across every backend and keeps the backend services focused on business logic. The gateway is also the natural place to attach a request ID if the client did not supply one, so every downstream log line and error response can be correlated to a single request. Behind the gateway sit the application services that implement the actual operations. A service validates the request body against its schema, checks authorization (this caller may read this resource but not write it), executes the operation, and serializes a response in the agreed format. For a REST API this is resource-oriented: a URL identifies a resource, an HTTP method states the intent, and the status code plus a problem+json body states the outcome. For GraphQL there is a single endpoint and a resolver layer that walks the query, fetching each requested field, typically with batching to avoid the N+1 problem. For gRPC the service implements methods defined in a protobuf contract and speaks a compact binary protocol over HTTP/2. In all three, the service is responsible for translating between its internal data model and the stable external contract, so that internal refactors do not leak through the API. Two concerns cut across the whole path and deserve first-class treatment. The first is idempotency and consistency for writes: an idempotency key supplied by the client is stored with the result of the first execution, so a retry of the same key returns the original result instead of performing the action again. The second is evolution: the API is versioned, the schema is published (OpenAPI, the GraphQL SDL, or protobuf files), and additive changes are made in place while breaking changes are isolated behind a new version so existing clients keep working. Supporting infrastructure includes a cache tier and CDN for safe reads (driven by ETags and Cache-Control), a rate-limit store (commonly Redis) holding per-caller counters, and an async subsystem for long-running work, where a slow operation returns an operation resource immediately and the client either polls it or is notified by webhook when it completes. Underneath, observability ties it together. Structured logs, traces, and metrics are keyed by the request ID, so a support engineer handed a request ID from an error response can find exactly what happened. The design goal across all of this is that the fast, common path (an authenticated read or a simple write) stays simple and cheap, while the harder guarantees (idempotent retries, backward-compatible evolution, async completion) are handled by dedicated mechanisms rather than bolted onto every handler.
In a real interview, sketch this on the whiteboard before diving into any single box.
Core components
Walk through each service. The interviewer wants to hear what each one owns, not just the names.
API gateway / edge
The single entry point that terminates TLS, authenticates callers, enforces rate limits and quotas, attaches or propagates a request ID, and routes to the owning service. Centralizing these policies here keeps them uniform across backends and keeps individual services free of auth and throttling boilerplate. It is also where you can do request/response transformation and version routing.
Authentication and authorization layer
Verifies who the caller is (API key lookup, OAuth2/JWT validation, or signature verification) and what they are permitted to do (scopes, roles, resource-level checks). Authentication is typically resolved at the gateway; fine-grained authorization often has to happen in the service, because only the service knows whether this specific caller owns this specific resource.
Request validation and schema
Validates every incoming request against a declared schema (OpenAPI for REST, the GraphQL type system, protobuf messages for gRPC) before any business logic runs, rejecting malformed or out-of-range input with a clear error. The same schema is published so clients can generate types and mocks, and it is the artifact that documents the contract.
Resource / business services
The services that implement the operations and own the mapping between the internal data model and the external contract. They execute the create, read, update, delete, and list logic, apply business rules, and produce responses in the agreed format. Keeping the external contract separate from internal models is what lets you refactor storage without breaking clients.
Idempotency store
A keyed store (often Redis or a database table) that records the idempotency key of each write plus the response produced the first time it ran. A retried request carrying the same key returns the stored response instead of re-executing, which is what makes retries after timeouts safe. Entries expire after a retention window such as 24 hours.
Rate limiter and quota store
Maintains per-caller counters over time windows (token bucket or sliding window) and rejects requests that exceed the limit with a 429 and Retry-After. Backed by a fast shared store so limits hold across many gateway instances. Quotas track longer-term consumption (per day or per month) for billing tiers.
Pagination, filtering, and sorting engine
The logic on collection endpoints that bounds result size and lets clients navigate large sets. Cursor-based pagination encodes a stable position (last seen key plus sort order) into an opaque token so paging stays correct even as items are inserted or deleted, which offset pagination cannot guarantee.
Async / long-running operation manager
Handles operations too slow to complete within a request. It accepts the request, returns 202 with an operation resource the client can poll (or registers a webhook), runs the work on a background queue, and updates the operation status to completed or failed. This keeps request timeouts short and decouples client latency from job duration.
Observability and error pipeline
Emits structured logs, traces, and metrics keyed by request ID, and formats all failures into a consistent problem+json body carrying a stable error code and the request ID. This is what lets a caller quote a request ID and an operator find the exact failure, and what turns errors from opaque 500s into something a client can program against.
Data model
Pick the right store per table. Justify each choice with the access pattern, not by reflex.
api_resource_definitionresource_nameversionschema (OpenAPI/SDL/protobuf)field_definitionsdeprecated_fieldsstatus (active/deprecated/sunset)The published contract for each resource and version. Field definitions include types, required flags, and per-field deprecation markers. Storing the schema as data (not just code) lets the gateway validate requests, generate documentation, and drive version routing, and lets you track which fields are deprecated and when they sunset.
api_client / api_keyclient_idhashed_api_keyowner_account_idscopesrate_limit_tierstatuscreated_atlast_used_atThe registry of who can call the API. The key is stored hashed, never in plaintext, so a database leak does not expose live credentials. Scopes define what the client may do and the rate-limit tier links to its quota. Rotating a key means issuing a new row and revoking the old one after a grace period.
idempotency_recordidempotency_keyclient_idrequest_fingerprintresponse_statusresponse_bodycreated_atexpires_atOne row per idempotent write. The request fingerprint (a hash of method, path, and body) detects the misuse of reusing a key with a different payload. On a retry the stored response is replayed; the row expires after the retention window. Scoping by client_id prevents one caller idempotency key from colliding with another.
rate_limit_counterclient_idwindow_startendpoint_classrequest_countquota_limitFast-changing counters, typically in an in-memory store like Redis with TTLs matching the window rather than a durable relational table. Separate endpoint classes (reads vs writes) can carry different limits. This is read and written on the hot path, so it must be cheap and shared across all gateway instances.
async_operationoperation_idclient_idresource_refstatus (pending/running/succeeded/failed)result_referrorcreated_atupdated_atThe handle returned for a long-running operation. The client polls this resource or is notified by webhook. result_ref points to the created resource on success and error carries a problem+json body on failure. Keeping operations as first-class resources gives long jobs the same consistent, pollable interface as everything else.
Deep dives
These are the conversations the interviewer is steering you toward. Practice each one until you can talk through it without notes.
REST vs GraphQL vs gRPC, and when each fits
These three are not competitors so much as answers to different questions. REST models the domain as resources addressed by URLs, uses HTTP methods for intent and status codes for outcome, and rides the entire HTTP ecosystem for free: caching, proxies, CDNs, and universal client support. It is the right default for public, resource-oriented APIs and anything that benefits from HTTP caching. Its weaknesses are over-fetching (an endpoint returns more than a given screen needs) and under-fetching (a screen needs three resources, so it makes three round trips). GraphQL answers the wish to let the client ask for exactly the fields it wants with a single endpoint, a typed schema, and a query language, which is powerful for mobile apps and rich frontends with many divergent data needs. The cost is that HTTP caching no longer applies cleanly (most requests are POSTs to one URL), you have to defend against expensive nested queries with depth and complexity limits, and the N+1 problem in resolvers has to be solved with batching. gRPC is contract-first binary RPC over HTTP/2 with protobuf: compact, fast, strongly typed, with streaming and code generation in many languages. It shines for internal service-to-service communication where both ends are yours and you want speed and a strict contract, but it is awkward for browsers (needs a proxy), not human-readable on the wire, and not something you usually expose as a public API. A staff-level answer picks by consumer: public and cache-friendly leans REST, diverse client-driven data needs lean GraphQL, internal high-performance calls lean gRPC, and many real systems use gRPC internally with a REST or GraphQL edge.
Resource modeling, naming, and HTTP method/status semantics
The backbone of a usable REST API is consistent resource modeling. Model nouns, not verbs: /orders and /orders/{id}/refunds, not /createOrder or /getOrderRefund. Use plural collection names, hierarchy to express containment (/customers/{id}/invoices), and keep names consistent in case and pluralization across the whole surface, because inconsistency is what makes an API feel untrustworthy. Map methods to intent honestly: GET is safe and never mutates, POST creates or triggers actions, PUT replaces a resource wholesale, PATCH applies a partial update, DELETE removes. Idempotency is a property of the method contract too: GET, PUT, and DELETE are defined as idempotent, POST is not, which is why creating with POST needs an idempotency key while PUT does not. Status codes must carry real meaning: 200 for success with a body, 201 with a Location header for a created resource, 202 for accepted-but-not-done, 204 for success with no body, 400 for malformed input, 401 for missing or bad authentication, 403 for authenticated-but-not-permitted, 404 for not found, 409 for a conflict such as a version mismatch, 422 for well-formed but semantically invalid input, 429 for rate limited, and 5xx only for genuine server faults. The classic mistake is returning 200 with an error inside the body, which breaks every client and intermediary that trusts the status line.
Versioning strategies (URI vs header vs media type)
Versioning exists to let you make breaking changes without breaking existing clients, and the first rule is to avoid needing it by making changes additively whenever possible. When a breaking change is unavoidable, there are three placements. URI versioning (/v1/orders, /v2/orders) is the most common and most visible: it is trivial to route, trivial to test in a browser, and unambiguous, at the cost of arguably violating the idea that a URL should identify a resource rather than a representation, and of proliferating paths. Header versioning (a custom header or an Accept-Version header) keeps URLs clean and lets the version travel as metadata, but it is invisible in a browser address bar, easy to forget, and harder to cache and debug. Media-type versioning (content negotiation via Accept: application/vnd.company.v2+json) is the most RESTful in spirit, treating the version as a representation of the same resource, but it is the least approachable for consumers and the least widely understood. Most large public APIs choose URI versioning for its bluntness and clarity, and some, like Stripe, use a date-based version pinned per account so a client keeps its behavior until it explicitly upgrades. Whatever you choose, the important disciplines are the same: never change the meaning of an existing field, add rather than mutate, and publish a clear deprecation policy so clients know when a version will sunset.
Pagination (offset vs cursor), filtering, and sorting
Any endpoint that returns a collection must be paginated, because an unbounded list is a latency and memory hazard and a denial-of-service vector. Offset pagination (limit and offset, or page numbers) is simple and lets a client jump to an arbitrary page, and it is fine for small, mostly-static datasets. It has two real problems at scale: deep offsets are slow because the database still has to scan and discard all the skipped rows, and the result drifts when items are inserted or deleted while paging, so a client can see duplicates or skip records. Cursor (keyset) pagination fixes both by encoding a stable position, typically the sort key of the last item seen, into an opaque cursor token that the client passes back to get the next page. The query becomes a request for the next 20 items after this key, which uses an index directly and stays correct under concurrent inserts and deletes. The trade is that you cannot jump to an arbitrary page and the cursor is opaque, which is a fair price for correctness and speed on large or live datasets. Filtering and sorting should be explicit and bounded: expose a defined set of filterable fields and sortable fields rather than allowing arbitrary expressions, because an open query language lets a client write an unindexed query that hurts everyone. Document which fields are filterable, validate them, and reject anything not on the list.
Idempotency and idempotency keys
Networks fail after the server has already done the work but before the client gets the response, so any client that retries can accidentally perform an action twice, and for a payment or an order that is unacceptable. Idempotency is the property that performing an operation more than once has the same effect as performing it once. GET, PUT, and DELETE are naturally idempotent by their contract. The dangerous case is POST, which creates or triggers. The standard solution is an idempotency key: the client generates a unique key (a UUID) for the logical operation and sends it in a header. The server, on first receipt, executes the operation and stores the key together with the result; on any retry carrying the same key, it skips execution and returns the stored result. Two subtleties separate a good answer from a great one. First, you must store a fingerprint of the request alongside the key and reject a reused key that comes with a different payload, because that means a client bug, not a retry. Second, you must handle the race where two requests with the same key arrive concurrently, typically by inserting the key with a uniqueness constraint so the second one detects the in-flight first and either waits or returns a conflict. Keys are retained for a bounded window (24 hours is common) because retries do not happen weeks later. Done well, idempotency keys turn retry-on-timeout from a dangerous gamble into a safe default.
Error format, RFC 7807 / RFC 9457 problem+json, and observability
An error is part of the API contract just as much as a success, and clients need to program against it, not parse prose. The standard is RFC 7807, Problem Details for HTTP APIs, now updated by RFC 9457, which defines the application/problem+json media type and a small set of fields: type (a URI identifying the problem class), title (a short human summary), status (the HTTP code), detail (a human-readable explanation of this specific occurrence), and instance (a URI for this occurrence), plus arbitrary extension members. The value is consistency: every error across your API looks the same, so a client writes one handler. In practice you add a stable, machine-readable error code (a string like rate_limit_exceeded or card_declined) that clients switch on, because the human-readable message will change over time and must never be parsed. Crucially, the error body should carry the request ID, and that same ID should appear in your structured logs and traces. This is the observability payoff: a caller reports a failure and quotes the request ID, and an operator finds the exact log line, trace, and downstream calls in seconds. Errors should also distinguish clearly between the client fault (4xx, do not retry without changing the request) and the server fault (5xx, safe to retry with backoff), and for retryable errors a Retry-After header tells the client when. Vague 500s with no code and no request ID are the difference between an API teams love and one they dread.
Authentication and authorization (API keys, OAuth2, JWT)
Every non-public endpoint must know who is calling and what they may do, and the three common mechanisms fit different situations. API keys are a single long-lived secret identifying a client, simple to issue and use, appropriate for server-to-server calls and machine clients; they must be sent in a header (never in the URL, where they leak into logs and history), stored hashed at rest, scoped to limit blast radius, and rotatable. OAuth2 is the framework for delegated access, where a user authorizes a third-party application to act on their behalf without sharing their password; the app receives a short-lived access token and a refresh token, and the token carries scopes that bound what it can do. It is the right choice whenever a human is granting an application access to their data. JWT is a token format, not an auth scheme: a signed, self-contained token whose claims (subject, scopes, expiry) can be verified without a database lookup, which makes it attractive for stateless, high-throughput authentication, with the well-known caveat that a stateless token cannot be revoked before it expires, so you keep expiry short and add a revocation list or introspection endpoint if you need instant revocation. The design line to hold is that authentication (who you are) usually resolves cleanly at the gateway, but authorization (may this caller touch this specific resource) often has to happen in the service, because only the service knows ownership. And authorization must be enforced server-side on every request, never inferred from the client, because anything the client controls, an attacker controls.
Rate limiting, quotas, and backward-compatible evolution
A public API needs to protect itself from both abuse and accidental overload, and it needs to evolve without breaking the people using it, and these two concerns share a theme of setting expectations clearly. Rate limiting caps request rate per caller over a window; a token bucket allows short bursts while bounding the sustained rate, and a sliding window gives smoother enforcement. When a caller exceeds the limit, return 429 with a Retry-After header and, ideally, RateLimit headers advertising the limit, remaining, and reset so a well-behaved client can self-throttle before it gets rejected. Quotas are the longer-horizon cousin, capping usage per day or per month, often tied to a billing tier. Backward-compatible evolution is governed by a simple contract: additive changes are safe (a new optional field, a new endpoint, a new optional parameter), and clients must be built to ignore unknown fields so that adding one never breaks them. Breaking changes (removing or renaming a field, changing a type, changing the meaning of a value, tightening validation) require a new version and a deprecation process: announce it, emit a Deprecation and Sunset header on the old surface, warn active integrators, give a generous window (often 6 to 24 months), and only then remove it. The most damaging mistake in API evolution is a silent semantic change, where a field keeps its name but starts meaning something different, because it passes every schema check and breaks clients in production with no warning.
Long-running operations, bulk/batch endpoints, and caching
Not every operation fits the synchronous request-response model, and forcing it to creates timeouts and brittle clients. For long-running work (generating a report, transcoding a video, provisioning infrastructure), the async pattern is to accept the request, return 202 with an operation resource that has its own URL and a status field, do the work on a background queue, and let the client either poll that operation resource until it reaches succeeded or failed, or register a webhook so the server calls the client back when it completes. Polling is simple and needs no inbound endpoint on the client; webhooks avoid polling overhead but require the client to expose and secure a receiver and require the server to retry deliveries and let clients verify signatures. Bulk and batch endpoints address the opposite problem, too many small calls: a batch endpoint accepts many operations in one request to amortize the per-request overhead, but it forces a decision about partial failure semantics (does the whole batch fail atomically, or does each item succeed or fail independently with a per-item result array), and the per-item result approach is usually more useful. Caching is the lever for read-heavy APIs: Cache-Control headers tell clients and intermediaries how long a response may be reused, and ETags enable conditional requests, where the client sends If-None-Match with the ETag it holds and the server replies 304 Not Modified with no body if nothing changed, saving bandwidth and work. ETags also power optimistic concurrency for writes through If-Match, so a client can ask to update a resource only if it has not changed since the version it read, which the server enforces by comparing the ETag and returning 412 Precondition Failed on a mismatch.
Trade-offs to discuss
Every senior interviewer expects you to surface at least 3 of these. Pick the decisions, state the alternatives, and justify your choice.
REST vs GraphQL for a client-facing API
REST gives you HTTP caching, universal tooling, and simple, cacheable resource endpoints, at the cost of over- and under-fetching that forces mobile and rich frontends into awkward multi-call patterns. GraphQL lets each client fetch exactly the fields it needs in one round trip, which is ideal when clients have divergent data needs, but you lose easy HTTP caching, take on resolver N+1 and query-cost problems, and have to defend against expensive nested queries. Choose REST when responses are cacheable and consumers are varied and external; choose GraphQL when a few known rich clients drive complex, differing data requirements.
Offset vs cursor pagination
Offset pagination is simple and supports jumping to an arbitrary page, which is fine for small, stable datasets, but deep offsets get slow and results drift when the underlying data changes mid-paging. Cursor pagination is index-friendly and stays correct under concurrent inserts and deletes, at the price of opaque tokens and no random page access. Choose offset for small admin-style lists where users expect page numbers; choose cursor for large, live, or high-throughput collections where correctness and performance matter more than jumping to page 50.
URI versioning vs header/media-type versioning
URI versioning (/v1/) is blunt, visible, trivially routable and testable, and universally understood, at the cost of cluttering the URL space and arguably conflating resource identity with representation. Header and media-type versioning keep URLs clean and are more RESTful in spirit, but they are invisible in a browser, easy to omit, harder to cache, and confusing for many consumers. Most public APIs choose URI versioning for its clarity; the header approach fits internal APIs where clients are disciplined and tooling is controlled.
Synchronous response vs async operation for slow work
A synchronous call is simple for the client and returns the result directly, but it ties client latency to job duration and breaks on timeouts once work takes more than a few seconds. An async operation returns immediately with a handle and decouples the client from job time, at the cost of a more complex contract (poll or webhook) and eventual-completion semantics the client must handle. Keep it synchronous when work reliably finishes within the request budget; go async the moment duration is variable or long, because a timed-out synchronous call is the worst of both worlds.
Stateless JWT vs opaque token with server-side lookup
A self-contained JWT can be verified without a database round trip, which is fast and scales statelessly, but it cannot be revoked before expiry, so a leaked or misused token stays valid until it times out. An opaque token checked against a store (or introspected) supports instant revocation and central control, at the cost of a lookup on every request. Choose JWT with short expiry for high-throughput internal auth where a small revocation delay is tolerable; choose opaque tokens or add an introspection/revocation layer when immediate revocation is a hard requirement.
Strict schema validation vs lenient acceptance (Postel principle)
Strict validation, rejecting any unexpected or malformed field, catches client bugs early and keeps the contract clean, but it makes the API brittle to additive change and can break clients that send harmless extra fields. Lenient acceptance, ignoring unknown fields and coercing where safe, makes the API robust to evolution and is what lets you add fields without breaking anyone, but it can hide client mistakes and let bad data through. The common resolution is asymmetric: be strict about validating semantically important input, but ignore unknown fields rather than rejecting them, so the API can grow additively without breaking existing callers.
Single-endpoint batch operations vs many individual requests
Individual requests are simple, cacheable, and independently retriable, but chatty clients pay per-request overhead and latency that adds up over hundreds of calls. A batch endpoint amortizes that overhead into one request, but it complicates the contract with partial-failure semantics, is harder to cache, and can create large, slow requests. Offer batch endpoints for known high-volume, latency-sensitive flows with clear per-item result reporting; keep individual endpoints as the default because they compose better with caching, retries, and rate limiting.
How API Design (designing a public HTTP/RPC API) actually does it
The design above tracks the public, authoritative literature rather than any one vendor internal claims. Roy Fielding 2000 dissertation defined REST as an architectural style derived from constraints (client-server, stateless, cacheable, uniform interface, layered system), and it is still the reference for what RESTful actually means, including why statelessness and a uniform interface are what make HTTP APIs scale and cache. The practical, opinionated guidance most large providers converge on is captured in Google API design guide and Microsoft REST API guidelines, which cover resource naming, standard methods, pagination, long-running operations, and error models in concrete terms, and in Stripe widely-studied public API, whose date-based versioning and idempotency-key design are frequently cited as best practice. The error format has a formal standard: RFC 7807, Problem Details for HTTP APIs, updated by RFC 9457, which many APIs adopt so that errors are consistent and machine-readable. For the contract artifacts themselves, OpenAPI is the dominant description standard for REST and JSON:API offers a full convention for resource documents, pagination, and relationships. An honest interview answer separates the timeless principles (resources, safe and idempotent methods, backward-compatible evolution, standard errors) from the pragmatic choices where reasonable APIs differ (URI versus header versioning, offset versus cursor pagination), and never claims a single approach is universally correct.
Lessons to study before this interview
If any of these topics are fuzzy, the interviewer will catch it. Each lesson is 15 to 60 minutes with diagrams, code, and a quiz.
REST API
foundation / core fundamentals
GraphQL
intermediate / api design protocols
gRPC
intermediate / api design protocols
API Versioning
intermediate / api design protocols
Idempotency Keys
intermediate / api design protocols
Rate Limiting
intermediate / api design protocols
Design an API Gateway
capstone / capstone
Related system design interview questions
Practice these next. They lean on the same core building blocks as API Design (designing a public HTTP/RPC API).