Should an HTTP API version the whole surface at once or version resources individually, and how do date-based pinned versions (as used by Stripe, where an account is pinned to a version like 2024-06-20) compare with numbered releases?
answer
- whole-API = simple docs, forced migrations, batching
- per-resource = matches change, matrix explodes
- date pin = per-account, upgrade is opt-in
- server holds current impl + ordered transformation chain
- some changes cannot be transformed
basics
~20 sWhole-API versions are simple to document and route but force migrations on customers whose endpoints did not change. Per-resource versions avoid that at the cost of a large support matrix. Date-based pinning gives fine-grained, continuous evolution with per-account pinning, at the cost of maintaining a transformation chain in the server.
solid answer
~60 s**Whole-API (`/v1` → `/v2`)** is one number to document, route and support, but it is coarse: a change to one endpoint drags every customer through a migration of code that did not change, so teams batch changes and versions bump rarely and painfully. **Per-resource versioning** lets orders reach v3 while payments stays v1. It matches how change actually arrives, but the support matrix and documentation explode, and consumers must track many numbers. **Date-based pinning** (Stripe-style): every change ships as a dated version; an account is pinned to the date it integrated on and keeps that behaviour indefinitely; upgrading is an explicit, per-account action, and a single request can override the pin with a header. Internally the server holds one current implementation plus an ordered chain of small **transformations** that downgrade the response (and upgrade the request) to the caller's pinned date. It is the best consumer experience — no forced migrations, small reviewable changes — and the highest engineering cost: every transformation is code and tests that must live for years.
go deeper
Know that most APIs bump one version for the whole surface, and that some providers pin customers to a dated version instead.
Contrast the three granularities and describe pinning plus the per-request override header.
Explain the transformation-chain implementation, its testing story, its latency and maintenance cost, and when each model is justified.
Set policy: how many versions are supported and for how long, what the carrying cost is, and which consumer profile justifies the pinned-version investment.
## Granularity is the real question "How do we encode the version?" is superficial. "What is the unit of change a consumer must react to?" determines cost for both sides. ## Whole-API versioning One version covers everything: `/v1/*` to `/v2/*`. Documentation is one set, routing is a prefix, SDKs pick a number, support answers "which version are you on?" with one field. The cost is coupling. Any breaking change anywhere forces a global bump, so a customer using ten endpoints must revalidate all ten because one changed. Rationally, teams respond by batching: versions become rare, huge, and dreaded, changes queue behind them for a year, and the API ossifies between releases. It also produces the odd artefact where `/v2/payments` is byte-identical to `/v1/payments` and exists only because the prefix moved. ## Per-resource versioning Each resource evolves independently: `/orders` at v3, `/payments` at v1. This matches reality — change is local. Consumers only migrate what they use. Costs land on the provider and on comprehension. The supported matrix is the product of all resource versions; documentation must render per-resource versions coherently; a consumer must track several numbers, and cross-resource consistency gets subtle when an order at v3 embeds a payment the caller reads at v1. Workable, but it needs real tooling before it pays off. ## Date-based pinned versions The model popularised by Stripe. Every backward-incompatible change ships as its own dated version, e.g. `2024-06-20`. An account is **pinned** at first integration to the then-current date, and stays there forever unless it explicitly upgrades. A per-request header can override the pin, which makes testing an upgrade trivial: replay traffic with the new date and compare. Upgrading is a deliberate action with a changelog listing exactly the deltas between your date and the target. **The internal mechanism is the point.** The server implements only the *current* behaviour. Each breaking change adds a small, isolated transformation module: given the current response, produce the older shape (and given an older request, produce the current shape). At request time the gateway looks up the caller's pinned date and applies, in order, every transformation newer than it. Core business logic never branches on version; all the compatibility lives in a chain of narrow, individually testable functions. Benefits: no forced migrations ever; changes are small and reviewable rather than batched into a mega-release; the same mechanism supports the entire long tail of old integrations. Costs: the transformation chain grows monotonically and every link is code and tests maintained for years; some changes cannot be transformed at all (a genuinely new data model, a removed capability with no old-shape equivalent) and need a real deprecation programme anyway; transformations add latency and a class of subtle bugs where a change is only *nearly* representable in the old shape; and it demands strong discipline — every change must be classified and, if breaking, given a transformation. ## Choosing Start with whole-API path versioning: it is cheap, familiar and fine while consumers are few and coordinated. Move to date-based pinning when you have many uncoordinated external customers, high change velocity, and a business cost to forced migrations — the classic profile of a payments or platform API. Per-resource numbering suits large internal platforms where teams own resources independently and a service catalogue already tracks the matrix. Whichever you pick, publish the support policy up front: how many versions or how many years of dates are supported, and how upgrades are announced. The carrying cost of old versions is the real budget line, and it is paid by the provider forever.
- How does a date-versioned server avoid version branches scattered through its business logic?It implements only the current behaviour and expresses each breaking change as an isolated transformation pair: downgrade the response to the older shape, upgrade the older request to the current shape. At the edge it applies every transformation newer than the caller's pinned date, in order. Domain code never sees a version, and each transformation is small enough to unit-test on its own.
- What is the main long-term cost of never forcing customers off old versions?The transformation chain and its tests grow without bound and must keep working through every future refactor, so today's cheap compatibility becomes a permanent tax on change. Some evolutions also cannot be represented in an old shape at all, so a deprecation programme is still eventually needed — pinning defers forced migrations rather than abolishing them.
saying these in an interview costs you the question
- Believing date-based pinning removes the need for deprecation entirely
- Implementing pinned versions as `if (version < x)` branches inside domain logic
- Assuming `/v2/foo` implies `foo` actually changed
- Adopting per-resource versioning without tooling to render and track the matrix
- Ignoring that every retained old version is carried cost paid by the provider, not the customer