Of everything that happened during the ARES zero-downtime migration, one discovery changed how I think about system boundaries — and it started with a sentence everyone believed:
“The v2 file id and the legacy reference number are basically the same thing.”
The assumption
Running two schemas side by side means every entity exists twice: once in the decade-old legacy tables, once in the new normalized schema. To keep clients working during the transition, APIs accepted and returned identifiers — and the comfortable belief was that for files, the new files.id and the legacy reference (islem_ref) were interchangeable. Same records, same numbers, no translation needed.
It’s the kind of assumption that feels safe precisely because everything seems to work. Most traffic hits recent records, recent records were created in both systems near-simultaneously, and near-simultaneous creation produces ids that are… close. Close enough to pass every casual test.
The measurement
Before building more on top of that belief, we did the boring thing: pulled 55 production files and compared their ids across both systems.
54 of the 55 differed.
Worse than differing — the two id ranges overlapped. A given number could be a perfectly valid file id in both systems, pointing at two different files. That’s not an error you catch with a “not found” response. That’s an API call that succeeds, returns a real file — the wrong one — and corrupts data downstream with complete confidence.
One overlap away from a claims adjuster editing someone else’s damage file. We stopped everything id-related that day.
Two-front fix
Front one: unify the file id space
For files themselves, we ended the dual identity at the schema level: the legacy reference was adopted as the canonical v2 file id — one number, one meaning, in both worlds. That decision rippled through 13 file foreign keys and another 22 relation and audit FKs, but it removed the single most dangerous ambiguity: from then on, “file 84213” meant the same record no matter who said it.
Front two: the wire contract
Files were only the loudest case. Every reference vocabulary the APIs spoke — vehicle makes and models, provinces and districts, occupations, policy types, insurance branches, document types, campaigns, invoices, regions — had its own legacy id space, and clients had legacy ids baked into requests, caches and saved filters.
You can’t flip all of that at once. So we ran an eight-phase wire contract program, one vocabulary family at a time:
- Ship the v2 ids alongside the legacy ones.
- Deploy dual-read mappers in every client — the React SPA, the Flutter tablet and the mobile app, simultaneously — so both id dialects are understood during the phase.
- Flip the API surface to emit v2 ids as canonical.
- Burn the legacy ids out of requests, then out of responses.
Some families were mechanical. Some — like insurance branches — needed live data remapping mid-phase. Each phase was small enough to verify and, critically, small enough to roll back.
What made it survivable
- Dual-read, not dual-write, in the clients. Clients never chose which id to send; they only had to understand both while a phase was in flight. Choice stayed on the server, where it could be changed without an app-store release.
- Auditability per phase. Each vocabulary flip had its own before/after checks — the same discipline as the table-family migrations, applied to identifiers.
- Naming the enemy. Once we called the problem “identity spaces”, conversations changed. “Which space is this id from?” became a standard review question — the cheapest bug-catching tool we added all year.
The takeaway
Every integration has identity spaces, and every long-lived migration will eventually put two of them in the same request. The failure mode isn’t an exception — it’s a successful call returning the wrong entity, which is as bad as data corruption gets while looking exactly like health.
So: never trust “they’re basically the same number”. Sample it. Measure it. Ours died on 55 files — and measuring cost an afternoon, while believing would have cost us production data.
Questions about migrations or API versioning? Reach out.