Back

Identity space wars: when two systems disagree about what things are | Veli Kaan Çetinel

· 4 min read

Migration API Design Architecture

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:

  1. Ship the v2 ids alongside the legacy ones.
  2. 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.
  3. Flip the API surface to emit v2 ids as canonical.
  4. 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

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.