Versioning data and APIs
Keeping old readers and old records working while things change.
01The intuition
The problem it solves
A school changes its report card from marks out of 100 to grades A to E. The new cards go out, but the parents' app still expects a number and now shows "NaN". The old cards in the archive still say 91, and a teacher comparing years cannot tell which scale a card used. Nothing was technically wrong with the new card; the mistake was changing the shape of a thing without saying which shape it is.
Systems live for years and everything in them changes: API fields, record layouts, configuration, documents edited by many hands. Versioning attaches a number (or a timestamp, or a hash) to every shape and every change so that old readers keep working, old records can still be read, and two edits to the same thing can be told apart.
Build it one piece at a time
Step 1: version the API. Put /v1/ in the path. New fields can be added to v1 responses because well-behaved clients ignore unknown fields; renaming or removing a field, or changing its type, requires /v2. Both run side by side until the last v1 client is gone, which you know because you count calls per version.
Step 2: version stored records. Every row carries schema_version. Code reads a row, checks the version, and upgrades it in memory if it is old. Writing it back stores the new version. Over time the old rows disappear without a risky all-at-once migration.
| Thing | Version carried as | Lets you |
|---|---|---|
| API | path or header, v1, v2 | change the contract while old apps keep working |
| record | schema_version column | migrate lazily, read old rows forever |
| document or config | a monotonically increasing number per object | detect conflicting edits and order changes |
| immutable content | a content hash | cache forever, share identical versions |
Step 3: version each object for concurrency. Two people edit the same restaurant menu. Each read returns version 7. Each write says "I am updating version 7". The first write succeeds and makes version 8; the second is rejected because version 7 no longer exists, and the client reloads and retries. This optimistic check costs nothing when conflicts are rare and never silently loses an edit.
Step 4: keep history. Instead of overwriting, append: version 8 is a new row (or a new blob) and version 7 stays. Now you have undo, audit, and "what did the menu say on Tuesday". Old versions are compacted or expired by a background job when they are no longer needed.
Step 5: version the rules too. A feature flag or a denylist is a versioned object: every server reports which version it is running, and a rollout is "move the fleet from version 41 to 42", which can be watched and reversed.
Why it works, with the numbers
A version turns "did anything change?" into an integer comparison, which is cheap enough to do on every request. Optimistic concurrency wastes work only in proportion to the conflict rate:
wasted writes per second ≈ writes per second × P(two writers hit the same version), tiny for menus, large for a hot counter
Lazy record migration spreads a change over the natural write rate: if 1 percent of rows are rewritten daily, 99 percent of rows are on the new schema after about 460 days, and a background sweep finishes the rest on your schedule, not during an outage. The guarantees: a reader never misreads a shape, two concurrent edits never silently merge, and a change can always be traced to a version. The failure modes are version numbers that are not monotonic (two servers both issue version 8), a client that hard-codes the latest version and breaks on the next, and keeping every version forever until storage fills.
The trade-off you must say out loud
Versioning is extra bookkeeping on every object and a period of running two shapes at once. Say how long two API versions coexist, how an old record is upgraded, and what a client should do on a version conflict. The interviewer's follow-up is often "what about a change that cannot be made additive?", and the honest answer is a new version, a migration path, and a date on which the old one dies.
Where it shows up in the 100 Google problems
- Design Google Docs: every edit is a numbered revision; the number is what lets clients detect they are behind and what makes history and undo possible.
- Design a global configuration and feature-flag service: servers report the config version they hold, so "has everyone got version 42?" is a count.
- Design an offline-first client: a per-object version is how the server tells "a new edit" from "an edit based on something old" and triggers a merge.
- Design a distributed IP denylist: edges fetch the list by version and skip the download when nothing changed.
- Design a data lake ingestion layer: schema versions on files let a year-old table be read by this year's jobs.
02Used in these problems
- Design a global restaurant menu update system
- Design Google Docs
- Design an offline-first client that syncs and resolves conflicts
- Manage user preferences across many Google products
- Design a source control system
- Design a global configuration and feature-flag service
- Design a distributed IP denylist and country ban enforcement
- Design a data lake ingestion layer
- Migrate live users from NoSQL to SQL without downtime