Global object ids must be re-encoded after a datastore migration — how do you roll that out?
answer
- Their lifetime is not your request
- You cannot enumerate every holder
- Accept both, emit one
- Measure the legacy share before retiring
- Retire an identifier, never recycle it
basics
~20 sNever in one deploy. Identifiers already handed out live in caches, links and other systems, so the server accepts both encodings for a long overlap, emits only the new one, and retires the old decoder when measured legacy traffic reaches zero.
solid answer
~50 sThe mistake is thinking an identifier's lifetime is the request. It is not: identifiers get stored in client caches, pasted into links, written into other services' rows and saved into dashboards, so their real lifetime is that of the longest-lived thing holding one. Flip the encoding in a single deploy and all of those go dead at once, surfacing as `node` returning null for values that worked yesterday. The safe shape is a **tolerant decoder**: recognise both encodings, resolve either to the same object, emit only the new one. Instrument the decoder so each lookup is counted by format, then retire the old branch once the legacy share has held at zero across a full turnover of the slowest holder. Two hard rules meanwhile: never reissue a retired identifier for a different object, and never make the tag inside it the SDL type name.
code
pseudocode · 18 linesfunction decodeGlobalId(value):
raw = base64Decode(value)
if raw is null: return NOT_FOUND
if startsWith(raw, "v2:"):
metrics.count("globalid.decode", format = "v2")
return parseV2(raw)
if legacyDecoderEnabled and looksLikeV1(raw):
metrics.count("globalid.decode", format = "v1")
legacy = parseV1(raw)
if legacy is null: return NOT_FOUND
newKey = legacyKeyMap.lookup(legacy.type, legacy.key)
if newKey is null: return NOT_FOUND
return (legacy.type, newKey)
metrics.count("globalid.decode", format = "unknown")
return NOT_FOUND # never guess; never fall throughgo deeper
Take away the core fact: an identifier you received may still be in use long after the request that produced it, so a server cannot quietly change how identifiers are formed.
Explain the accept-both-emit-one shape and why a version marker inside the identifier removes the guesswork a decoder would otherwise need between two schemes.
Show the rollout as steps with evidence: decoder first, instrument by format, backfill any key mapping, flip emission, retire on a measured zero — and name the cross-type mis-resolution you are guarding against.
Frame the identifier as a published contract with the same change discipline as the schema, and push the real fix upstream: version it from day one and never derive it from anything renameable or editable.
## Why this is a senior question Because the wrong answer sounds fine. "We changed the encoding and shipped it" is a defensible sentence about almost any internal format, and a wrong one here, for a reason that only shows up in production: **you do not control where your identifiers are stored.** The point of Global Object Identification is that an identifier can leave the request that produced it. Once you have advertised that, every identifier you have ever emitted is potentially still alive somewhere you cannot enumerate. ## Where they actually live On a solar-array telemetry graph, a single panel identifier had come to rest in: an operator's browser tab open since the previous shift, a saved dashboard definition, a link in an incident ticket, a row in a work-order service that had stored the graph identifier as a foreign key, and a mobile client's offline store. None of those are yours. Their turnover ranges from minutes to months. ## The failure modes, in order of nastiness **Silent nulls.** The tolerable case. The old encoding no longer decodes, the lookup returns null, and clients that handle a missing object degrade. You find out from a support ticket rather than a page, which is its own problem — at a 1,200-request-per-minute peak a few percent of lookups going null is invisible on a success-rate chart. **Cross-type resolution.** The bad case. If old and new encodings can both parse successfully but disagree about which part is the type tag, an old identifier can decode into a *different* valid identifier and return the wrong object. That is worse than a null in every way, because nothing errors and the wrong data renders. It is the argument for a version marker inside the identifier from the very first release: one byte or one prefix that says which scheme this is, so the decoder never has to guess. **Reuse.** The worst case. If a retired identifier is later reissued for a different object, every stale holder silently resolves to the wrong thing forever. Identifiers must be retired, never recycled. ## The rollout 1. **Add the new encoding to the decoder first, and only the decoder.** Deploy a server that accepts old *and* new and still emits old. This step changes no output, so it is safe to roll back. 2. **Instrument the decode path.** Count every lookup by which branch parsed it, labelled by concrete type. Without this you are guessing about step 5. 3. **Backfill any mapping you need.** If the local key itself changed — an integer primary key becoming a UUID, say — the old branch needs a lookup table from old key to new. That table is the migration; keep it as long as you keep the old branch. 4. **Flip emission.** Deploy the server that emits the new encoding. From here the legacy share only falls. 5. **Watch it decay, then retire.** Retire the old branch when the legacy count has been zero across a period longer than the turnover of your slowest known holder — and remember that includes systems that copied identifiers into their own rows, which never turn over on their own. Those need a migration of their own or an explicit agreement. ## What makes this avoidable next time **Version the identifier from day one.** A scheme marker costs a byte and turns a guessing decoder into a dispatching one. **Do not derive the identifier from anything renameable or editable.** The two classic sources of a forced re-encoding are a type rename — if the tag inside the identifier is literally the SDL type name, renaming `SolarPanel` to `PvModule` invalidates every identifier of that type — and a local key taken from mutable business data such as a serial that gets re-stamped after a repair. Keep an internal stable tag and map it to the SDL type name, and use a key the business cannot edit. Neither costs anything up front, and both remove entire classes of migration. **Treat the identifier as a published contract.** It has the same change discipline as a field in the schema: additive changes are cheap, changes of meaning are not, and removals need an announced window. ## What to say about the null One more judgement call an interviewer may probe: what should the server do for an old identifier after the old branch is retired? Return null. It is the honest answer and it matches the field's nullable return type. Do not throw an error carrying the parse failure, and above all do not fall back to a lenient parse that might land on some other object — the whole migration exists to avoid exactly that.
- How do you decide the old encoding is safe to retire?By measurement, not by calendar. Label every decode with the format that parsed it and watch the legacy share fall; retire only after it has held at zero for longer than the turnover of the slowest holder you know about. Holders that copied identifiers into their own storage never decay on their own, so they need either their own backfill or an explicit agreement before the branch comes out.
- Should renaming an SDL type change the identifiers of its objects?No, and designing so that it must is the mistake. Keep an internal, stable tag inside the identifier and map it to the current SDL type name, so a rename is a schema change rather than a mass invalidation of every identifier of that type. If the tag already is the type name, treat a rename as a full re-encoding migration and run the same accept-both rollout.
- Is it ever acceptable to reuse a retired global identifier for a different object?No. Stale clients, links and other services still carry it, and a reused value makes every one of those resolve silently to the wrong object — strictly worse than the null they would otherwise get, because nothing signals the mistake. Identifiers are retired, not recycled, even when the underlying local key is genuinely free again.
- What should the server return once an old identifier no longer decodes?Null for that lookup. It matches the field's nullable return type and is the honest report of "no such object". Do not raise an error carrying the parse failure, which documents your encoding to anyone probing, and never fall back to a lenient parse that might resolve to some other object — the migration exists precisely to prevent that.
Identifiers you have handed out are like street numbers printed on other people's business cards: you can renumber the street, but not by Tuesday, and not without keeping the old numbers routable while the cards are still in circulation.
saying these in an interview costs you the question
- Assumes an identifier's lifetime is one request
- Flips the encoding in a single deploy
- Reuses a retired identifier for a new object
- Ships no version marker inside the identifier
- Puts the renameable SDL type name in the identifier
- Guesses between encodings when a parse is ambiguous