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.
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.