Engineering

Why data changes are riskier than code changes

Editorial · Reveneau · October 10, 2026

Why data changes are riskier than code changes

There is an asymmetry at the centre of software operations that most teams' processes do not reflect.

A bad code deployment is a ninety-second problem. The previous version still exists, in the registry, in the artefact store, in git. You roll back, the old behaviour returns exactly, and you diagnose in the morning.

A bad data change is permanent. The column you dropped is gone. The rows your backfill overwrote are gone. Rolling back the deployment does nothing at all, because the deployment was never the thing that changed.

Everybody knows this when asked directly. Few pipelines act like it.

The rollback that is not a rollback

Most migration frameworks give you an "up" and a "down". The down migration is presented as the reverse of the up, and that framing is where the trouble starts, because reversibility is a property of the specific change rather than of the mechanism.

Some changes genuinely reverse. Adding a nullable column, adding an index, creating a table: the down migration removes what the up migration added, and the world is exactly as it was.

Some changes reverse in shape only. Dropping a column is the clearest case. The down migration recreates the column, correctly typed, with the right name, holding nothing. You have restored the structure and lost the information, which is the part anyone cared about.

Some changes do not reverse in any useful sense at all. A backfill that normalises a free-text field into an enumeration, running once over four years of accumulated data, has destroyed the original strings. The down migration can drop the enum column. It cannot tell you what was in the text field.

When a rollback plan says "revert the migration", the useful question is which of those three it means. Often nobody has asked.

Where generated migrations go wrong

This asymmetry matters more now, not less, and the reason is about how review works.

Reviewers rely on surface cues to decide how carefully to read. Unfamiliar style, awkward naming, a comment that trails off: these are the signals that make a person slow down. Generated code has none of them. A migration produced by a model is fluent, conventionally structured, correctly named, and reads exactly like something a careful engineer wrote after checking.

The checking is the part that may not have happened. A generated migration will confidently drop a column on the reasonable-looking assumption that nothing reads it, add a NOT NULL constraint on the assumption that no existing row is null, or write a backfill in a single transaction on the assumption that the table is small. Each assumption is plausible, each is stated in clean code, and none of them is verifiable by reading the diff. They are claims about the data, and the data is not in the pull request.

Veracode's spring 2026 testing across more than 150 models found syntax correctness above 95 percent while the security pass rate sat near 55 percent and had been flat for two years. The shape of that gap is the shape of the problem here. Producing code that is well-formed turns out to be a different skill from producing code that is safe against conditions the author cannot see. We covered the broader version of this in why AI-generated code fails code review.

Four controls that make data changes survivable

Separate the data change from the deployment

Ship them as two events, with two decisions and two blast radii. A deployment that also runs a migration has one failure mode wrapped around another, and when something goes wrong at 2am the first thing anyone has to work out is which one broke.

Separating them also makes the deployment genuinely rollback-able again, which is the property you wanted in the first place.

Never destroy in the same step that stops using

The expand-and-contract sequence, and it is worth the extra deployments.

Stop writing to the column. Ship it. Let it run for long enough that any weekly job, monthly report, or quarterly export has had a chance to touch it. Confirm from real query logs that nothing reads it. Then, in a change that does nothing else, drop it.

Each step is individually reversible. The pause is not ceremony: it is the window in which you find the reader nobody remembered, which is almost always a scheduled job or somebody's dashboard rather than application code.

Run it against production volume before it matters

A migration tested against 200 rows in a development database has been tested for correctness and not for anything else.

The behaviours that cause incidents are functions of size. A table alteration that takes 40 milliseconds locally may hold an exclusive lock for eleven minutes against 11 million rows, during which every write blocks and the application is effectively down. A backfill that runs in one transaction locally may exhaust the transaction log in production. None of that is visible at small scale.

Restore a recent production backup into a scratch environment and run the migration there, with a timer on it. The number you get is the number you plan around. Anything else is a guess with a decimal point on it.

Measure recovery instead of assuming it

"We have backups" and "we restored one last month, it took 40 minutes, and the application started cleanly against it" are different statements, and only the second one is a control.

Restore on a schedule, into an isolated environment, and record the elapsed time. The things that go wrong are mundane and all of them are discovered the same way: the retention window is shorter than anyone thought, the encryption key was rotated, the backup is of a schema version the current code cannot read, or the restore takes six hours and the recovery objective says one.

An incident is the worst moment to learn any of these. It is also, in most organisations, the only moment they get learned. Why observability matters more with generated code makes a related argument about measuring rather than assuming.

A different review standard

Code review asks whether a change is correct and well designed. Data change review has to ask five more questions, and they do not come up when reviewing a user interface change, which is why one shared checklist covers them badly.

What information does this destroy, and can it come back? Name the specific columns, rows, or files. "Nothing" is an acceptable answer and should be stated rather than assumed.

How long does it run at production volume, and what does it lock? With a measured number, from a real run against real data sizes.

Does the application work with both schemas during the rollout? For any deployment that is not instantaneous, and none of them are, old and new code run simultaneously. Both must work.

What is the recovery procedure, and when was it last performed? The date matters as much as the procedure.

Can this be split into smaller reversible steps? If the answer is no, that deserves a sentence explaining why, because the answer is usually yes.

The habit worth building

None of this is sophisticated. It is a sequencing discipline and a willingness to spend three deployments on something that could technically be done in one.

What makes it hard is that the cost is visible and immediate while the benefit is invisible and deferred. Nobody gets credit for the outage that did not happen. The team that splits every destructive change across three deploys looks slower than the team that does it in one, right up until the afternoon the second team drops a column that a quarterly finance export reads, and finds out eleven weeks later when the export runs.

Code changes are experiments you can undo. Data changes are decisions you cannot. Treating them the same way is the mistake, and it is one that stays invisible for a long time before it stops being invisible all at once.

Sources

Common questions

Why is a data migration riskier than a code deployment?

Because code is replaceable and data is not. Rolling back a deployment restores the previous behaviour exactly, since the previous version still exists. Rolling back a migration restores the previous structure but cannot restore information that was deleted or overwritten, so the recovery path runs through a backup rather than through the deployment pipeline.

Can you roll back a database migration?

You can usually reverse the structural part, such as recreating a dropped column or renaming a table back. What a reverse migration cannot do is recover the contents of what was destroyed, so a down migration on a destructive change gives you the old shape holding nothing. Treat reversibility as a property of the specific change rather than of migrations in general.

How should destructive schema changes be sequenced?

Split them across separate deployments with time in between. Stop writing to the column, ship that and let it run, confirm nothing reads it using real traffic data, then drop it in a change that does nothing else. Each step is individually reversible, and the pause is what gives you the chance to notice a reader you forgot about.

Why test migrations against production-sized data?

Because the behaviours that hurt are functions of volume. A migration that runs in 40 milliseconds against 200 development rows may hold an exclusive lock for eleven minutes against 11 million production rows, taking the application down while it runs. Correctness and duration are separate questions and only the first one is answered by a small dataset.

What makes AI-generated migrations particularly risky?

They are syntactically correct and fluent, which removes the surface cues reviewers use to slow down. A generated migration will confidently assume a column is unused, that a value is never null, or that a backfill can run in one transaction, and each assumption is stated in clean code that reads as though somebody verified it. The check has to come from evidence about the actual data rather than from reading the diff.

What is a backfill and why does it go wrong?

A backfill populates a new column or table with values derived from existing data. It goes wrong because it usually runs once against rows written under years of different rules, so the edge cases it meets are the accumulated history of every bug and manual fix the system ever had. Running it in batches, making it resumable, and logging what it skipped are the difference between a fixable backfill and an unrepeatable one.

How do you verify a backup is actually usable?

Restore it. Into a separate environment, on a schedule, and write down how long the restore took and whether the application started against it. A backup that has never been restored is an untested assumption, and the first restore attempt during an incident is the worst possible moment to discover the retention window, the encryption key, or the schema version is wrong.

Should data changes be reviewed differently from code?

Yes, and by different criteria. Code review asks whether the change is correct and well designed. Data change review should also ask what is destroyed, whether that destruction is reversible, what the change does at production volume, who is affected while it runs, and what the recovery procedure is. Those questions do not come up when reviewing a user interface change, so a single review checklist covers them badly.

What should a migration review checklist contain?

What data is destroyed or overwritten and whether it can be recovered. How long the change runs against production volume and what it locks. Whether the application works with both the old and new schema during the rollout. What the recovery procedure is and when it was last performed. Whether the change can be split into smaller reversible steps, and if not, why not.

Does this change with zero-downtime deployment tooling?

Tooling helps with the structural mechanics and does not change the underlying asymmetry. Online schema change tools can add a column without locking a large table, which removes one class of problem. They do not make a dropped column recoverable, and they do not make a bad backfill undoable, so the sequencing discipline is still what keeps the change survivable.