interviewpath
  • 0day streak
  • 10total XP
  • Level 1

Focus mode

API contracts and compatibility

Read alongside

Google API Improvement Proposals (AIPs): AIP-158: Pagination

Short, opinionated guidance on resource design, pagination, request identifiers, errors, freshness checks, compatibility and versioning; read the listed AIPs as design checklists.

Objectives

  • Design a resource-oriented API for a small domain, specifying request and response schemas, validation and an error model with stable machine-readable codes.
  • Compare offset and cursor pagination and explain what each returns when rows are inserted or deleted between page requests.
  • Implement idempotency keys for a non-idempotent create operation, including storage, replay of the original response, concurrent duplicates and reused keys with different payloads.
  • Use ETags and conditional requests to prevent lost updates when two clients edit the same resource.
  • Classify proposed API changes as backward compatible or breaking, and explain forward compatibility for clients and stored data.
  • Choose a versioning and deprecation strategy for a given set of clients and justify it.

Why it matters

Once another team, a mobile app or a customer integration calls your endpoint, its observable behavior becomes the contract, including details you never meant to promise. Changing it later means coordinating with every client, so mistakes made in the first design review are expensive. API questions, and the API step of system design rounds, reveal whether you think about failure (what happens when the client times out and retries?), concurrency (what does page three mean while rows are being inserted?) and evolution (how do you add a field next quarter?), not only about URL naming.

Core ideas

Contracts and validation. Model resources and the operations on them, make required fields, formats, ranges and limits explicit, and validate at the boundary so bad input fails fast with a precise message. Return errors in one structured shape: an HTTP status for the class of failure, a stable machine-readable code, a human-readable message and field-level details. RFC 9457 (Problem Details) and Google's AIP-193 are two documented conventions. Never leak stack traces or internal identifiers.

Pagination. Offset pagination (LIMIT 20 OFFSET 40) is simple and allows jumping to page N, but the database still walks the skipped rows, and a row inserted before the current position makes the client see a duplicate while a deletion makes it skip one. Keyset or cursor pagination remembers the last sort key returned and asks for rows after it, for example WHERE (created_at, id) > ($1, $2) ORDER BY created_at, id LIMIT 20. It stays fast on deep pages and does not shift when earlier rows change, but it needs a unique, stable ordering (add the primary key as a tie-breaker) and cannot jump to arbitrary pages. Neither scheme is a snapshot: a row updated after you passed it is not revisited. If clients need a consistent view, page over a read timestamp or a materialized result. Wrap the cursor in an opaque token so its internals can change, reject tokens used with different filters, and cap the page size.

Idempotency. Networks fail after the server commits but before the client hears back, so clients retry. A POST that creates a payment is not idempotent, so the standard fix is a client-generated idempotency key per logical operation. The server stores the key with a fingerprint of the request and the eventual response; a repeat with the same key returns the stored result instead of acting again, and a repeat with a different payload is rejected. Claim the key with a uniqueness constraint so two concurrent duplicates cannot both proceed, mark it in progress until the work finishes, and where possible commit it in the same transaction as the side effect. Keep keys longer than any client's retry horizon.

Concurrency control. Lost updates happen when two clients read, modify and write the same resource. Return an ETag (a version) and require If-Match on updates; the server answers 412 Precondition Failed if the version moved, and the client re-reads and retries deliberately.

Compatibility and versioning. Backward compatibility means new servers still serve old clients; forward compatibility means old readers tolerate data or responses produced by newer writers, usually by ignoring unknown fields. Adding an optional field or a new endpoint is normally safe. Removing or renaming fields, changing types or units, tightening validation, adding a required request field, or changing defaults and error codes breaks someone. During a rolling deployment old and new server versions run side by side, so even internal APIs need compatibility across adjacent versions. Reserve new major versions for unavoidable breaks, announce deprecations with dates, and measure who still calls the old version before removing it.

Common misconceptions

  • "PUT and DELETE are idempotent, so retries are always harmless." The intended effect repeats safely, but responses can differ (a second DELETE may return 404), and a blind retried PUT can overwrite a newer write unless it carries a precondition.
  • "Cursor pagination gives a consistent snapshot." It prevents shifting pages, but concurrent updates can still move rows across the cursor or change their content.
  • "An idempotency key is just a deduplication cache." Without an atomic claim, two concurrent requests with the same key can both execute; without a payload fingerprint, a buggy client can reuse a key for a different operation.
  • "Adding an enum value can never break anyone." Clients that switch exhaustively over the values may fail on an unknown one; document that enums can grow and make clients handle unknown values.
  • "Versioning solves compatibility." Every live version is code to run, test and secure; additive design avoids most new versions.

How to approach it

Read Google's AIPs on pagination, request identification, errors and backwards compatibility, and Stripe's API reference on idempotent requests as a worked example of a retry contract. Then write a one-page spec for a small resource you know, such as outreach campaigns with messages: list and get, create with an idempotency key, update with a precondition, and an error table. In interviews, state the contract before the implementation, name the retry and pagination semantics explicitly, and offer a compatibility plan for the first change you expect. Graduate candidates are expected to get methods, status codes and pagination right; mid-level candidates should handle idempotency and conflicts; senior candidates are pushed on evolving an API across many clients and on how the guarantees hold during deployments.

Self-check before you finish

  1. What does a client see with offset pagination if a row on an earlier page is deleted between requests?
  2. Why must the pagination cursor include a unique column, and what goes wrong without it?
  3. What should a server do when an idempotency key is reused with a different request body?
  4. Which header and status code implement optimistic concurrency for updates?
  5. Why do internal APIs still need backward compatibility during a rolling deployment?
  6. When is a new major API version justified, and what does it cost?