advanced 3 min answer

A public API must keep every historical version working for thousands of integrators while the team keeps shipping breaking changes. Compare date-pinned per-account versioning with URL versioning and feature flags.

stripeapi-versioningbackward-compatibilitytransformationsintegrators
Show the full answer Hide the answer

The three models

URL versioning (/v1/, /v2/) is the most common and the least suited to a large integrator base. It batches unrelated breaking changes into a single migration event, so an integrator who cares about one change must absorb all of them at once. The result is universally observed: v1 never dies, because nobody can justify the migration, and the team maintains two or three complete implementations indefinitely.

Date-pinned per-account versioning assigns each account a version — a date — set on first use and never changed without the account's action. A breaking change creates a new version dated today; existing accounts keep the version they had. New integrators get current behaviour; existing ones are untouched.

Feature flags control behaviour per account without a version concept. Useful for gradual rollout, terrible as a versioning mechanism: the combinatorial space of flag states becomes untestable, and nobody can say what an account's API actually does without querying its flag set.

Why date pinning wins for a large integrator base

  • Changes are decoupled. Each breaking change is independent; an integrator upgrading moves through them in order rather than absorbing a batch.
  • Nobody is forced to migrate on the provider's schedule, which is what actually makes it possible to ship breaking changes at all.
  • The current version is always the good one, so new integrators are never onboarded onto legacy behaviour — the quiet failure of URL versioning, where new customers pick v1 because it is documented better.
  • The upgrade is testable: an account can request a specific version per request, so an integrator can run their test suite against the new version before committing.

How it is implemented without N implementations

The core is written once, against the current version. Each historical version is a transformation layer — a small piece of code that converts the current request/response shape to and from that version's shape.

Requests flow forward through transformations from the account's version to current; responses flow backward from current to the account's version. A version is a pair of transformation functions, not a copy of the API.

This is the property that makes the approach sustainable: the cost of supporting a version is one small, independently-testable transformation, not a parallel implementation that must receive every bug fix.

What it costs

  • Every breaking change requires writing a transformation, and some are genuinely hard — a change that adds information that did not previously exist may have no faithful backward mapping. Those changes must be designed to be additive instead, which is a real constraint on the API's evolution.
  • The transformation chain grows. An account many versions behind passes through many transformations, with latency and correctness risk accumulating.
  • Testing is a matrix: every endpoint against every supported version. This must be automated from the version definitions, or it will not be done.
  • Eventual deprecation is still necessary, and is now a per-account conversation rather than a single announcement.

The principle underneath

Make the cost of a breaking change fall on the party making it, not on thousands of integrators. A transformation layer is the provider absorbing that cost deliberately, and it is why this model appears specifically at companies whose integrators' failure is the company's failure.