Compatibility Layers — Serving Twenty Versions from One Implementation

Promising account-pinned dated versions is easy; the hard part is the server that must honor all of them. The naive answers — fork the codebase per version, or thread `if version < X` through every handler — both collapse within a year. The architecture that scales is a transform pipeline: one current implementation at the core, and a chain of small, testable version-gates that translate requests forward and responses backward. This post builds it.

Post 2 chose the scheme that makes this layer mandatory; this post is the price being paid — and why it’s lower than every alternative.

The transform-chain architecture

The organizing idea: the core implementation speaks only the latest version. Business logic, storage, new features — all written once, against current shapes. Each dated version (post 2) contributes a version gate: a pair of pure transforms — one mapping an older-version request forward into the next version’s shape, one mapping a newer-version response backward into the older shape. A request from an account pinned at 2025-03-01 passes forward through every gate between its date and today before reaching the core; the response runs the same chain in reverse. The properties that make this scale:

Where transforms get hard

Pure-shape renames are the easy case; the taxonomy of harder gates is worth knowing before committing. Semantic changes — a field’s meaning shifted (amounts to minor units, timezone conventions): the gate converts values, not just names, and needs the property-testing attention of any money/time code. Information asymmetries — the new response lacks something the old version promised: the gate must reconstruct it (compute, refetch, or synthesize a faithful default); conversely, old requests may lack data the new core requires — defaults encode what the old era implicitly assumed. This is where gates occasionally need context (a lookup, a secondary read), breaking the pure-function ideal — contained by keeping such gates rare, flagged, and measured. Behavioral changes — different status codes, altered validation strictness, pagination semantics: gates may wrap error translation and even request replay logic; some behavior (timing, rate limits) can’t be gated at all and must be versioned by policy exception, documented as such (post 1’s behavioral contract has limits). Fan-in/fan-out changes — one endpoint split into two, or two merged: the gate orchestrates (calls the core twice, stitches responses), the most complex species and the strongest argument for making such restructures rarely.

Operating the chain

The layer earns its keep operationally. Version telemetry (post 2’s invariant) now measures each gate: traffic per version, latency added per gate (chains of pure transforms are cheap, but measure — a context-needing gate in a hot path is a found performance bug), and error rates by version (a spike isolated to old versions means a gate regression, not a core one — a diagnostic gift). Version-matrix testing in CI runs the golden suites for all supported versions on every change — the mechanical guarantee behind “we still serve 2024 correctly,” and the thing that makes refactoring the core safe (gates pin observable behavior per era). And retirement reverses the chain: when post 5’s deprecation process finally ends a version, its gate is deleted — the chain shortens, the oldest fixtures retire, and the system sheds history it no longer owes. The architecture thus gives API evolution the property zdm-07 gave migrations: changes stop being heroics and become units — authored, priced, tested, and eventually retired through machinery built once.

The takeaway: serving many versions scales only through a transform chain — a latest-only core wrapped in per-version gates, each a small, pure, fixture-tested pair of request-forward and response-backward transforms, composed in date order. Know the hard species in advance (semantic value conversion, information asymmetries needing context, behavioral wraps, fan-in/out orchestration) and keep them rare; operate the chain with per-gate telemetry and all-version golden CI; retire versions by deleting gates. One implementation, twenty eras, every era tested — that’s the machinery that makes continuous breaking change a routine instead of a reorg.

Key takeaways

Further reading

Sources & References

translation-at-the-boundary, generalized by the gates
the request-pipeline architecture the chain lives in