Version Transformation Layer
also called Version Shim, Compatibility Transform, Request/Response Rewriting
Implementing an API once against its current shape and expressing every historical version as a pair of request and response transformations, so supporting an old version costs a small testable function rather than a parallel implementation.
The naive approach to supporting multiple API versions is to keep multiple implementations. This fails predictably: every bug fix must be applied N times, the implementations diverge, and the oldest version becomes code nobody understands and nobody dares delete.
A version transformation layer inverts the structure. The business logic exists once, in its current form. Each supported version is defined by two functions: one that converts an old-shaped request into the current shape, and one that converts a current-shaped response back into the old shape.
A request from an account pinned three versions back passes forward through three transformations into the core, and the response passes backward through the same three on the way out.
Why it matters
It changes the marginal cost of supporting a version from "a fork of the system" to "two small pure functions." That difference is what makes it economically possible to support every version ever issued, which in turn is what makes it possible to ship breaking changes at all.
The second-order effect is the important one: when the provider absorbs the cost of change, integrators are never forced to migrate on the provider's schedule. That is the property that keeps thousands of integrations working while the API continues to evolve — and it is the reason this pattern appears specifically at companies whose customers' failure is their own failure.
Implementation patterns
- Version as an ordered list of transformations, applied in sequence, so each breaking change contributes exactly one transformation and versions compose.
- Pure functions with no I/O, which makes them fast, individually unit-testable, and safe to chain.
- Pinning at the account level, set on first use, with a per-request override header so integrators can test against a newer version before committing.
- Generated compatibility tests: every endpoint × every supported version, produced from the version definitions rather than hand-written, or the matrix will not be covered.
- A rule that new fields are additive, since additive changes need no transformation at all — the cheapest version is the one that was never needed.
- Recorded real traffic replayed through the transformation chain to verify that old accounts see byte- identical responses after a change.
- Usage telemetry per version, so deprecation targets the accounts that actually exist rather than the versions that theoretically do.
Industry example
Stripe's API is the widely-studied example: accounts are pinned to a dated version, breaking changes create a new dated version, and existing integrations continue to receive the behaviour they were built against. Requests and responses are transformed between the account's version and the current internal representation, so the core implementation reflects only the present.
The observable consequence is unusual and instructive: a company shipping frequent breaking changes to a payments API used by an enormous number of integrators, without periodic mass-migration events — which is the outcome URL versioning consistently fails to produce.
Failure scenarios
- A change with no faithful backward mapping — most often one that adds information which did not previously exist, or that removes a concept the old shape requires. These must be redesigned as additive changes, and discovering this late is the pattern's main practical constraint on API evolution.
- Transformations with side effects or I/O, which makes chains slow and non-deterministic.
- Long chains for very old accounts, accumulating latency and subtle correctness risk.
- An untested matrix, where a transformation two versions back silently breaks and is discovered by a customer.
- Leaking the current shape — an error message, a webhook payload or a nested object that bypasses the transformation and exposes new-version fields to an old-version account.
- Never deprecating anything, so the chain grows without bound because the cost of each version is small enough to ignore.
- Version pinning applied to the API but not to webhooks or client libraries, which are separate surfaces with the same problem.
Trade-offs
The pattern constrains how the API may evolve. Any change that cannot be expressed as a reversible transformation is effectively forbidden, which pushes designs toward additive evolution — usually healthy, and occasionally a genuine limitation when the right change is a restructuring.
It also adds a permanent layer to the request path, with latency and a class of bugs that exist only in the transformation code. And it requires organisational discipline: every breaking change must ship with its transformation and its tests, which is real work at exactly the moment the team wants to move on.
The trade is a permanent structural cost borne by the provider in exchange for never imposing a migration on integrators. For an internal API with three known consumers this is unjustifiable overhead — a coordinated change is cheaper. For a public API whose integrators cannot be coordinated, it is close to the only model that works.
Interview question
"We ship a breaking change to our public API roughly monthly and we have four thousand integrators. Design the versioning, then tell me what you would do about a change that adds a required field which older versions have no way to supply."