
Schema Evolution Breaks When the Version Overlap Is Unmeasured
A schema change is usually one of the most difficult types of change for a software system, and ByteByteGo's "Schema Evolution: Changing the Contract Without Breaking What Runs" opens with the honest reason why: the migration goes cleanly in staging, and unrelated services start failing in production, and nothing was wrong with the migration itself (ByteByteGo, "Schema Evolution: Changing the Contract Without Breaking What Runs", Aug 20, 2026, https://blog.bytebytego.com/p/schema-evolution-changing-the-contract). The failure is not the change. The failure is the assumption that only one schema version is in play. Rows written years ago are read by code that has since been replaced. Messages sitting in a queue were published before the current consumer was written. Mobile app versions from eighteen months back are still installed and still calling the API. The property "no breakage on schema change" is guaranteed exactly when the mechanism — backward and forward compatibility plus expand-and-contract sequencing — is implemented and measuring whether two versions are still alive. Theorem 3: a property is guaranteed exactly when its mechanism is implemented and measuring.
Key Conclusions
- Schema changes break production not because the migration is wrong but because it takes effect while two versions of the application are still running against the same database, and only one of those versions referenced the modified schema (ByteByteGo, Aug 20, 2026).
- More than one schema version is always in play: rows written years ago, messages queued before the current consumer, mobile apps eighteen months old still calling the API — data written under one schema version is read under another.
- "No breakage" is a property that holds exactly when backward and forward compatibility are implemented as mechanisms and the version overlap is measured. A schema change without a compatibility mechanism is a contract change without a guarantee.
- Theorem 3: a property is guaranteed exactly when its mechanism is implemented and measuring. Expand-and-contract is the mechanism; the deprecation timeline that tracks the last old reader is the measurement.
More than one schema version is always in play
The ByteByteGo intro names the structural fact that every schema-change postmortem rediscovers: the migration goes smoothly in staging, and unrelated services fail in production, and the investigation finds nothing wrong with the migration — it took effect while two versions of the application were still running against the same database, and only one of those versions referenced the modified schema. That is not a staging-production gap. That is a one-version assumption meeting a multi-version reality. Staging tests the migration against the new code. Production runs the migration against the new code and the old code and the old data the old code wrote and the queued messages the old publisher wrote. The staging environment is a single-version topology; production is a version-overlap topology.
[UNIQUE INSIGHT] The version overlap is not bounded by the deploy window. The ByteByteGo intro is explicit: rows written years ago can get produced by application code that has since been replaced. Messages sitting in a queue were published before the current version of the consumer was written. Mobile app versions from eighteen months back are still installed on real devices and still calling the API. The deploy window is the smallest overlap; the durable-state overlap — old rows, queued messages, installed mobile clients — is the overlap that actually breaks you. A schema-change plan that only accounts for the deploy window is measuring the smallest version overlap and asserting the property over the largest. That is the gap between the mechanism that is implemented (deploy-window coordination) and the mechanism that would guarantee the property (full version-overlap tracking, including durable state and installed clients).
The Honest Architect reads the ByteByteGo claim as a topology claim, not a process claim. "More than one schema version is always in play at the same time" is a statement about the shape of a distributed system: writers and readers are decoupled in time, and the schema is the contract that spans the time gap. A change to the contract is safe only if it spans every reader that is still alive, including readers that were alive when the data was written and readers that will be alive when the data is read. Backward and forward compatibility are two different mechanisms, not one.
Backward and forward compatibility are two different mechanisms
Backward compatibility is the property that new code reads old data. Forward compatibility is the property that old code reads new data. They look symmetric in name and they are not symmetric in mechanism. Backward compatibility is a property the new-code author can guarantee by tolerating missing fields — the new code knows what the old schema looked like. Forward compatibility is a property the new-code author cannot guarantee alone, because the old code is already deployed; the only guarantee is to make the change in a way the old code already tolerates (add optional fields, do not remove fields the old code reads, do not change field semantics). Backward compatibility is a write-time guarantee; forward compatibility is a design-time constraint on the change itself.
The ByteByteGo article promises to cover "which changes break consumers, which do not, and the qualifiers that decide it" — and the qualifiers are the mechanism. Adding an optional field is backward-compatible (new code reads old data that lacks the field) and forward-compatible (old code reads new data that has an extra field it ignores). Renaming a field is neither: old code reads new data and looks for the old field name, finds it missing, breaks. Removing a field the old code reads is forward-incompatible. Changing a field's type or semantics is both backward- and forward-incompatible even if the wire bytes are the same, because the contract is the meaning, not the bytes. The Honest Architect treats a rename or a type change as a contract break, not a schema change — it violates an assumption a live reader is making.
[PERSONAL EXPERIENCE] Everythink's wire types are defined once, in Zod, in @everythink/types, and responses are parsed at the network boundary. That is a schema registry at the boundary, not just a type definition. The Zod schema is the contract; parsing at the boundary is the measurement — a payload that does not match the schema surfaces as a typed ApiError, never a crash. We tag that Production ✅ because the mechanism (parse-at-boundary) is implemented and the measurement (the typed error) runs on every response. A schema change at the backend without a matching Zod-schema change is a contract change without a consumer-side measurement — Partial ⚠️ until the Zod schema is updated and the parse-at-boundary catches the drift.
The Oracle's invariant — probabilities are normalized in exactly one place, everythink-oracle::ensemble, so consumers may rely on sum(probability) ≈ 1.0 — is a schema contract that downstream code reads. A change to the normalization location or the sort order would be a contract break even if the wire bytes looked identical, because the contract is the guarantee consumers rely on. We tag that invariant Production ✅ because the mechanism (single normalization site) is implemented and the measurement (the ensemble tests) runs. The schema-evolution discipline for that invariant is: never move the normalization site without an expand-and-contract sequence that lets old consumers keep reading the old guarantee while new consumers read the new one.
Expand and contract — the topology move
Expand and contract is the mechanism that makes a contract change safe under version overlap. Expand: add the new schema element in a way that is both backward- and forward-compatible. Let old readers keep reading the old contract and new readers start reading the new one. Wait for the version overlap to drain — old clients to update, old messages to be consumed, old rows to age out. Contract: remove the old schema element once no live reader references it. The expand phase is the topology expansion (two routes coexist); the contract phase is the topology contraction (one route remains).
The measurement that gates the contract phase is the deprecation timeline that tracks the last old reader. ByteByteGo promises "versioning strategies and deprecation timelines" — and the deprecation timeline is the measurement that makes expand-and-contract a guaranteed property rather than a hope. The Honest Architect tags an expand-and-contract plan without a last-old-reader measurement Partial ⚠️: the mechanism (expand, wait, contract) is implemented, but the measurement that gates the contract is not. A plan that tracks the last old reader — by client-version telemetry, by queue message age, by row schema-version tags — and contracts only when that count hits zero is Production ✅.
[ORIGINAL DATA] The Everythink migration discipline tags this directly. Every .up.sql migration has a matching .down.sql — that is the rollback path, the contract that a failed expand can be undone. The .down.sql is the expand-phase safety net: if the expand breaks a live reader, you contract the schema change (run the down migration) and the old readers resume. We tag the rollback path Production ✅ because every migration has one and the migrator enforces the pairing. A migration that adds and drops in one step is a cutover, not an expand-and-contract — Partial ⚠️ if the old code is still live, because the contract phase runs inside the version overlap.
The World Monitor geo cache carries the same discipline in a different shape. GeoSignal ids are deterministic uuidv5(source, native_id) — re-ingest updates, never duplicates. That is a forward-compatibility mechanism for the cache: a new ingestion of the same signal updates the row instead of creating a second row, so a reader that saw the old id and a reader that sees the new id are reading the same row. The id-stability is the contract; the deterministic uuidv5 is the mechanism; the upsert is the measurement (the row count does not grow on re-ingest). We tag that Production ✅ because the mechanism is implemented and the row-count stability is observable. A schema change to the id scheme would be a rename of the primary key — the most forward-incompatible change there is — and would require an expand-and-contract that writes both ids and migrates readers before dropping the old.
What an Honest Architect reads in a paywalled intro
The ByteByteGo article is paywalled past the "Version Overlap" section header, and the Honest Architect does not fabricate the body. What is visible is the structural claim — more than one schema version is always in play, the migration is clean in staging and breaks in production, the failure is not the migration but the version overlap — and that claim is enough to apply the tagging discipline. The visible intro gives: the multi-version reality (Production ✅ as a structural fact of distributed systems), the staging-production gap as a single-version-vs-multi-version topology difference (Production ✅ as a framing), and the promise of backward/forward compatibility, expand/contract, schema registries, and deprecation timelines as the mechanisms.
The Honest Architect rule: cite the real source, never fabricate a URL or metric, never claim the article said something it did not. The intro says the migration goes cleanly in staging and breaks in production because two versions are running against the same database. That is the cited claim. The rest of the analysis is the mechanism applied to Everythink's own stack, tagged against our own implementation, not attributed to ByteByteGo. The cross-domain claim (expand-and-contract is the same topology move in a database schema and in a forecast ensemble) is Partial ⚠️ because the form is shared and the domains are separate.
The scope limit: Everythink is a civil-and-defensive forecasting platform, not a database consultancy. The schema-evolution lesson is cross-domain — a contract change is safe only when the version overlap is measured — and it applies whether the contract is a database schema, an API payload, a Zod type, or a forecast-ensemble invariant. No token, wallet, or community-credit outcome is promised; those are Roadmap 🔵, subject to Howey review. A Roadmap item is never quietly promoted to Production on the strength of a schema-evolution form.
Frequently Asked Questions
Why does a schema change break production when the migration ran cleanly in staging?
Because staging tests the migration against the new code only, and production runs the new code alongside old code, old data, queued messages, and installed mobile clients that were written under the old schema. The migration is clean; the version overlap is not. The failure is not the change — it is the assumption that only one schema version is in play. Theorem 3: the property "no breakage" is guaranteed only when the version-overlap mechanism is implemented and measuring.
What is the difference between backward and forward compatibility?
Backward compatibility is the property that new code reads old data — the new-code author guarantees it by tolerating missing fields. Forward compatibility is the property that old code reads new data — the new-code author cannot change the old code, so the only guarantee is to make the change in a way the old code already tolerates (add optional fields, do not remove or rename fields the old code reads). Backward compatibility is a write-time guarantee; forward compatibility is a design-time constraint on the change itself.
What is expand and contract?
Expand: add the new schema element in a way that is both backward- and forward-compatible. Let old and new readers coexist. Wait for the version overlap to drain — old clients to update, old messages to be consumed, old rows to age out. Contract: remove the old schema element once no live reader references it. The expand phase is a topology expansion (two contract versions coexist); the contract phase is a topology contraction (one version remains).
How does Everythink enforce schema evolution at the wire boundary?
Wire types are defined once, in Zod, in @everythink/types. Responses are parsed at the network boundary; a payload that does not match the schema surfaces as a typed ApiError, never a crash. The Zod schema is the contract; the parse-at-boundary is the measurement. A backend schema change without a matching Zod-schema change is a contract change without a consumer-side measurement — Partial ⚠️ until the Zod schema is updated.
Does Everythink promise that schema changes never break consumers?
No. We promise the mechanism: Zod parse-at-boundary, .up.sql/.down.sql pairing, single-normalization-site Oracle invariant, deterministic uuidv5 for the World Monitor cache. The property "no breakage on a given change" holds when the change follows expand-and-contract and the deprecation timeline tracks the last old reader. A change that cuts over inside the version overlap is Partial ⚠️. No token, wallet, or community-credit outcome is promised; those are Roadmap 🔵.
Sources
- ByteByteGo, "Schema Evolution: Changing the Contract Without Breaking What Runs", Aug 20, 2026, retrieved 2026-08-23, https://blog.bytebytego.com/p/schema-evolution-changing-the-contract
If your team is ready to measure the version overlap, not just the deploy window, create your network — the topology routes two contract versions, the boundary parses, the rollback path undo.

Customization is the mechanism separation, not the open weights
Inkling is designed to be customized not because of its Apache 2.0 license but because each architectural decision isolates a measurable property behind its own mechanism. The bias-based load balancing is the purest Theorem 3 instance: a property guaranteed by a mechanism that does not compete with the main objective.
→ →
Geolocating a MAC Address Needs the Mechanism, Not the Identifier
A MAC address does not contain GPS, but a wardriving database plus a signal-weighted centroid merge can geolocate a fixed access point. Theorem 3: the property comes from the mechanism, not the identifier.
→ →
Data Enrichment Is Coherence, Not Volume
More data does not automatically mean better insight. Theorem 3: the property (better insight) comes from the mechanism (coherence-checking across data points), not from the volume. The value is better questions, not certainty.
→ →Build your world on an engine that proves what it claims.
Create your own network on the engine that's run since 2016 — or talk to the team behind the 21 papers.
