Schema Evolution Lab

Add, rename, remove or retype a field, and watch which consumers break — including the ones that break without failing. Producers change schemas because a feature shipped; consumers find out because a number moved.

A contract is not a schema file. A schema says what shape the data has; a contract says who promised what to whom, what happens when the promise is broken, and where that is enforced. Without the last part, a schema registry is documentation — and a change that violates it still reaches production, because nothing was standing at the boundary.

The decision to make in advance, per hop, is what a violating batch does: rejected at the boundary, quarantined for inspection, or passed through with the damage. All three are defensible and only one of them is a decision if you make it during the incident.

The lab

Make a change to the producer's schema and see it propagate — through the serialization format, the raw layer, the transformation and the consumers.

Schema evolution lab

Pay attention to the consumers that stay green. Those are the ones that will be wrong for months, because nothing in the platform is going to raise its hand about them.

Schema evolution lab
The useful axis is not breaking against non-breaking. It is loud against silent — and a contract's whole job is moving changes from the second column into the first.
Change
BREAKS SILENTLYSend a number as a string
The cast produces null rather than an error, so the rows survive, the row counts match, and every amount silently becomes nothing.
With a contract: The batch is rejected because the payload no longer matches the agreed type. A silent wrong number becomes a loud outage — the correct trade, and it will still page someone at 3 a.m.
ChangeNo contractWhy
compatibleReaders that do not know the column ignore it; readers that do get nulls for old rows, which is what they should get.
breaks loudlyOld rows have no value for it, so anything that enforces the requirement rejects the history it already holds.
breaks loudlyAnything that selects it fails. The failure is loud, immediate, and lands on whoever reads it rather than whoever removed it.
BREAKS SILENTLYTo a reader this is a drop and an add at the same time. Selects on the old name fail, and any SELECT * pipeline quietly starts carrying a new column full of the values the old one used to hold.
compatibleEvery old value fits in the new type. This is the direction evolution is meant to go.
BREAKS SILENTLYValues that fit continue to work. Values that do not are truncated or wrapped, and the row survives with a different number in it.
BREAKS SILENTLYThe cast produces null rather than an error, so the rows survive, the row counts match, and every amount silently becomes nothing.
BREAKS SILENTLYThe type is unchanged, the range is plausible, and every check passes. This is the change no schema system can catch, because the schema did not change.
The producer sends `amount` as a string — run through the pipeline model
Published
yes
Reported total
0.00 minor unitssim
True total
934,498.90 minor unitssim
Validity check
FAIL
amount arrived as a string. The cast produced null rather than an error, so every row survived with no value in it.
Enforcing the contract does not make the change safe. It makes it visible — the run fails instead of publishing a table of nulls, and somebody is woken up. That is the trade, and taking it is the point: an outage you can see is cheaper than a number you cannot.
SIMULATEDFORMAT-SPECIFICWhether a given change is tolerated depends on the serialization format and the reader: Avro resolves renames through aliases, Parquet readers match by name, and a JSON consumer notices nothing until a value is missing. The row for `amount` as a string is run through the pipeline model; the rest is a declared taxonomy.

Four changes, ordered by how loudly they fail

The loud ones get reviewed. The silent ones are the ones still costing you something a quarter later.

Add a nullable fieldfails loudly
Who breaks

Usually nothing — but a consumer doing SELECT * into a fixed-width structure, or asserting an exact column set, breaks immediately.

What to actually take from it
The safest change available, and still not free. Compatibility is a property of the serialization format and the consumer's assumptions together, never of the change alone.
Remove or rename a fieldfails loudly
Who breaks

Every consumer that referenced it, at once, loudly.

What to actually take from it
The loud break, and therefore the cheap one. The expensive part is not the failure — it is that nobody could say in advance which eleven dashboards referenced the column, which is a lineage problem rather than a schema one.
Change a typefails silently
Who breaks

Nothing visibly. The cast produces null rather than an error, so every row survives, row counts match, and the measure becomes nothing.

What to actually take from it
The signature failure of this domain. It passes every count-based check there is, and it is only visible to a check that looks at values.
Change what a field meansfails silently
Who breaks

Nothing at any layer. The name is the same, the type is the same, and the number moves.

What to actually take from it
No schema check can catch this, because nothing about the schema changed. Only a distribution check or a human who knows the domain will notice, and only if they are looking.

Where to go next