Skip to main content

Concepts

API design: idempotency, pagination, and contract evolution

Intuition

An API is a contract between whoever provides a service and whoever uses it, and the network between the two fails: the request can be lost, the response can be lost, and the caller can only retry. Think of pressing the elevator button: pressing it twice does not call two elevators; the result is the same as pressing it once. A good API is designed this way: repeating a request is safe, reading a long list in pages neither loses nor repeats items even if the list changes during the read, and the service can improve over time without breaking those who already use it.

How it works

HTTP method semantics (RFC 9110) already carry part of the contract: GET, HEAD, and OPTIONS are safe (they do not change state), and GET, HEAD, PUT, and DELETE are idempotent (running them N times has the same effect as running them once), while POST and PATCH are not by default. For non-idempotent operations that must not be duplicated, such as charging a payment, the client generates one unique idempotency key per intent and sends it in a header; the server stores the key together with the result of the first execution, atomically with the effect, and, when it receives the same key again, returns the stored result instead of executing again (and rejects the same key with a different body). This is what makes retry safe under at-least-once delivery: the client repeats until it gets a response, and the server deduplicates, the same reasoning that demands idempotent steps and compensations in a saga. In pagination, offset pagination (page N = skip N items) is simple, but inserts or deletes during the read shift the items and cause repeats or omissions, and it also costs more at large offsets. Cursor pagination returns an opaque token that encodes the position of the last item seen, over a stable and unique ordering (for example sort key plus id), and the next page starts right after that position, so inserts before it shift nothing. In contract evolution, compatible changes (adding an optional response field, a new endpoint, a new optional parameter) do not break clients that ignore unknown fields; incompatible changes (removing or renaming a field, changing a type or semantics, making a parameter required) demand a new version, whether in the URL path, in a header, or in a media type, with a deprecation period. Finally, the contract includes behavior under failure and overload: a rate limit responds 429 with Retry-After so the client backs off, and errors follow a consistent format, with the correct HTTP status, a stable machine-readable code, and a message for humans, distinguishing errors worth retrying (5xx, 429) from those that are not (4xx validation).

In practice

In any public API or between services, the practical rules are: make operations with an expensive side effect (payments, order creation, message sending) idempotent with an idempotency key, use a cursor on large or constantly changing collections, and treat the published schema as a promise, evolving by addition and versioning only when unavoidable. Canonical references: RFC 9110 for the semantics of methods, statuses, and headers; Fielding's REST thesis for the motivation behind a uniform, stateless interface; and the idempotency keys documentation of a payments API as a model of how a server deduplicates retries. This is the concept to choose when the client is a third party you do not control, where every contract break becomes an incident on their side.

Use it in a design

Practice the cases that rely on this concept in the System Design school.