Your team keeps two versions of one endpoint's payload running side by side for a year. What actually doubles, and what does not fork?
answer
- cheap on day one, expensive later
- shapes fork, records do not
- cost is versions times future changes
- adapters at the edge, one model inside
- meaning reaches both projections
basics
~20 sWhat doubles is the surface: contracts, tests, docs, client support and every later change, which must land in both. What does not fork is the data and the behaviour behind them, so a change of meaning reaches old callers however many shapes you serve.
solid answer
~40 sSide-by-side versioning is cheap on day one and expensive on day two hundred. The **shapes** fork - two contracts, two sets of examples, two test matrices, two support paths - and so does every subsequent feature, because each one must be designed against both or explicitly declared newest-only. The **substrate** does not: both versions almost always project the same stored records and call the same logic behind a translation layer at the edge. That is why side-by-side handles *representational* differences well - a renamed, nested or re-typed field - and handles *semantic* differences badly: if you change what a value means, both shapes carry the new meaning and the old caller is broken while its contract looks untouched. The design that survives is one internal model plus a per-version adapter, never two stacks.
go deeper
Recall that a service can answer in two payload shapes at once, and that both shapes are usually built from the same underlying records.
Explain what multiplies - contracts, tests, docs, and every subsequent feature - and why the translation belongs in a thin layer at the boundary rather than in a forked service.
Show what side-by-side cannot hide: changes in meaning reach both projections, and without per-version telemetry from day one there is no evidence for a shut-off date.
Own the policy: how many versions may exist at once, whether new features are backported, who pays the ongoing cost, and what the end date is before the second version is approved.
## What side-by-side actually is Running two versions side by side means the service accepts and emits two payload contracts at once, selected by whichever version marker you chose, for as long as callers need the older one. It is the alternative to migrating everyone in lockstep, and its appeal is obvious: the old caller keeps working with no work on its side. The question an interviewer is really asking is whether you know what you have signed up for. ## What doubles - **The published contract and its examples**, including error shapes, which are the ones people forget. - **The test matrix.** Every behaviour now has two representations to assert, and the interesting bugs live in the translation between them. - **Every later change.** A new field, a new validation rule, a new error condition: each must be designed against both shapes, or consciously declared to exist only in the newer one - which is itself a decision somebody must take and record each time. - **Support and on-call surface.** A report of wrong data now starts with "which version was that caller on?", and the answer must be available in logs and metrics, which is a design requirement, not a courtesy. - **Client tooling and documentation** - generated clients, sandboxes, examples, tutorials - all of which rot in the version nobody uses any more. The cost is not two; it is roughly *versions x future changes*. That product is what makes a "temporary" second version expensive, and it grows even if traffic on the old version is near zero. ## What does not fork - **The stored records.** Both versions normally read and write the same underlying data. One representation is not a copy of the other; it is another projection. - **The behaviour.** Validation, side effects, authorization and business rules sit behind the translation layer and are shared. - **Operational reality.** A degraded dependency, a schema change in storage, a capacity limit - all of it hits both versions at once. This is the part candidates miss, and it has a sharp consequence. ## The consequence: shape versus meaning | Kind of change | Can side-by-side hide it from an old caller? | |---|---| | Field renamed | Yes - the adapter maps the name | | Field moved into a nested object | Yes - pure restructuring | | Numeric field widened or re-typed | Usually - the adapter converts, if the old range still holds | | Field removed from the newer shape | Yes - the older shape keeps emitting it | | **What a field means** (units, time base, rounding, status semantics) | **No** | | **When a side effect happens** | **No** | A semantic change propagates through the shared substrate into both projections. The old caller's contract is textually unchanged and its behaviour is not, which is worse than an honest break: nothing in the payload signals it. Semantic changes therefore need the expand-and-contract treatment inside the data itself, or a genuinely separate value, not a second wrapper. ## How to run it so it can end 1. **One internal model, adapters at the boundary.** Two full stacks diverge within months and then every fix has to be written twice, badly. The adapter should be the only place that knows a version exists. 2. **Instrument per version and per caller from the first day.** The shut-off decision is a query about who is left, and the data must already exist when you want to ask. 3. **Declare a policy for new features before the second version ships** - newest-only by default, backported only by exception - so the doubling is bounded by design rather than by argument. 4. **Put an end date on it at birth.** A version with no planned end has no owner, and the cost is paid by everyone who touches the service afterwards. Side-by-side is the right tool when the difference is genuinely representational and the caller base is wide and slow. It is the wrong tool when what changed is what the system means, and it is a trap when it is chosen to avoid the conversation about who migrates and when.
- Why are two full stacks worse than one model with per-version adapters?Because they diverge. Every fix, every security patch and every dependency upgrade must be applied twice, and the copies drift until the older stack quietly behaves differently in ways nobody has tested. Adapters confine version knowledge to a translation layer that is small enough to read.
- What should the default policy be for a new feature while two versions run?Newest version only, with backporting as a recorded exception. Without that default, every feature reopens the argument and the older shape grows in step with the newer one, which defeats the point of having a newer one and pushes the shut-off date further away.
- A change to what a field means has to ship. What do you do instead of a new endpoint version?Introduce it as a distinct value rather than a redefinition: add a new field carrying the new meaning, leave the old field meaning what it always did, and migrate readers across it with the usual phases. A second wrapper around one redefined value fools nobody and tells nobody.
saying these in an interview costs you the question
- Assumes a second version isolates old callers from every change
- Forks the whole service rather than adapting at the boundary
- Counts the cost as two contracts instead of two times every change
- Ships a second version with no planned end date
- Adds no per-version telemetry until the shut-off is being planned
- Uses a new version to hide a change in what a field means