FREE LESSON · Applications & software architecture · 6 OF 11

API contracts and compatible change

API contracts and compatible change: Treat schemas, errors, ordering, and retries as observable behavior.

Prerequisites: Functions, HTTP basics, and sets.

Learning objectives

  • Specify success and failure semantics for an API.
  • Find a compatibility failure beyond field names.
  • Plan an additive migration with measured deprecation.

A contract is bigger than a JSON shape

An API contract includes accepted inputs, meanings of outputs, authorization, error behavior, ordering, pagination, and retry semantics. A response can pass schema validation while breaking a client that relied on sorted results. A field can retain its type while changing units from seconds to milliseconds. Compatibility is about preserved behavior for supported clients, not merely successful parsing.

Suppose GET /reservations returns active reservations sorted by created_at. A timestamp alone is not a total ordering when two records share it. Add an ID as a tie breaker and define the pair as the order. A cursor can encode the last pair. The next page selects records after that pair. Offset pagination over concurrently inserted data can duplicate or omit items because offsets refer to positions that move.

Trace a concurrent pagination case

Initially the ordered IDs are [10,20,30,40]. A client requests two items and receives [10,20]. Before its second request, ID 15 is inserted. Offset 2 now yields [20,30], duplicating 20. A cursor meaning after ID 20 yields [30,40], assuming IDs are the stable ordering key in this toy example. This does not make the traversal a historical snapshot: the new 15 will not appear. Decide whether the use case requires a snapshot, a live traversal, or eventual discovery through another synchronization mechanism.

Request contracts need equally explicit failure cases. A client that receives a timeout cannot infer that a create operation failed. Use an operation identifier when retries must be deduplicated. Bind that identifier to both caller and canonical request payload. Reusing it with a different payload should produce a conflict, not silently return an unrelated result.

Evolve with readers and writers in mind

Adding an optional response field is often compatible with clients that ignore unknown fields, but strict decoders may reject it. Adding a new enum value can break exhaustive client switches. Removing a field or changing a required input is more obviously disruptive. Test actual client behavior rather than assuming that a syntactically additive change is harmless.

For a representation change, first deploy readers capable of handling old and new forms. Then change writers, migrate stored data if needed, observe errors, and finally remove the old path after its supported lifetime. Record an explicit rollback point. If a migration discards information, rolling back the application binary alone cannot recover it. Contracts make these dependencies reviewable before a release turns them into incidents.

Practice problem

Write a contract for a paginated reservation list with concurrent inserts. Specify ordering, cursor contents, authorization, and whether traversal represents a snapshot. Design a compatible transition from a string status to a larger set of statuses.

Worked solution

Use a stable total order such as (created_at,id), an opaque cursor bound to the query, and server-side filtering by the authenticated reader. Document that a live cursor traversal may omit inserts before its position, or implement a snapshot if that stronger promise is needed. For statuses, inspect decoder behavior, add an explicit unknown fallback where appropriate, publish the new value before using it, and keep old semantics during a supported transition. Do not silently collapse a new state into a contradictory old one.

Check your understanding

Why can offset pagination repeat an item after an insert?

  • Offsets refer to changing positions.
  • JSON cannot represent integers.
  • Every insert changes existing IDs.
  • HTTP forbids stable sorting.
Explanation

The insertion shifts the positions used by the next offset; stable key-based traversal avoids that specific failure.

Which additive change can still break a client?

  • Adding a response field that every client ignores.
  • Adding a new enum value to a client with an exhaustive switch.
  • Keeping the existing units.
  • Returning the documented error code.
Explanation

An exhaustive decoder or switch can reject an unfamiliar value even when the schema change appears additive.

Primary reading

Open the study and save your work →