skip to content

A schema migration reaches production and turns out to be wrong. When is a scripted rollback actually a viable recovery, and when do you instead forward-fix by shipping a new migration?

level: seniorimportance: must knowfreq 55%

answer

  1. structure reverses, data does not
  2. additive + nothing depends on it + minutes = rollback ok
  3. forward-fix is the exercised path
  4. copy or rename before dropping
  5. PITR = break glass, loses recent writes

basics

~20 s

Rollback is viable only for additive, reversible changes that no code or data depends on yet — usually within minutes. Once a drop or a data transformation ran, or new code wrote data in the new shape, undoing loses information, so you forward-fix with a new migration and, if needed, roll back the application.

solid answer

~60 s

The honest position is **forward-fix by default, rollback as a narrow special case**. Rollback is genuinely viable when all of these hold: the change was purely additive (a new table, a nullable column, a new index); nothing has written data that only the new shape can hold; and you are undoing it promptly. Then `DROP INDEX` / `DROP COLUMN` restores the previous state exactly, and a Liquibase `rollback` block or a hand-written down script does the job. Rollback is a trap when the migration **destroyed or transformed information** — a dropped column, a `UPDATE` that normalised values, a type narrowing that truncated. The reverse script recreates a *shape*, not the *data*; you cannot un-drop a column's contents. It is equally a trap once the new application version has written rows in the new format, since reversing throws that writing away. So: keep destructive steps out of the deploy that introduces them, make the application tolerate both shapes, and correct mistakes by appending a migration. Reserve true rollback for restoring from backup/PITR — a decision about data loss, not about scripts.

code

sql · 8 lines
sql
-- V41__stash_legacy_ref.sql
CREATE TABLE customer_legacy_ref_backup AS
SELECT id, legacy_ref FROM customer;

ALTER TABLE customer DROP COLUMN legacy_ref;

-- V60__drop_legacy_ref_backup.sql   (weeks later, once confident)
DROP TABLE customer_legacy_ref_backup;

go deeper

for a junior

Know that a down script can undo an added column or index but cannot bring back data from a dropped one, and that teams usually fix forward.

for a middle

Separate transactional rollback of a failed migration from deliberate reversal of a committed one, and state the additive-and-recent condition for reversal.

for a senior

Argue forward-fix as default, describe copy-before-drop and rename-before-drop to buy a reversal window, and explain rolling back the application instead of the schema.

for a principal

Position it as a recovery-strategy question: what the RPO/RTO are, when PITR is authorised, how deploy sequencing keeps code rollback always available, and who decides during an incident.

## Two different meanings of "rollback" Candidates conflate three things: 1. **Transactional rollback** — a migration that fails mid-statement is undone by the database. On engines with transactional DDL (PostgreSQL, SQL Server) this is automatic and complete. On MySQL or older Oracle, DDL commits implicitly, so a multi-statement migration can leave the schema half-changed. This is free and always desirable; it is not what the interview question is about. 2. **Scripted rollback / down migration** — a second script that undoes the first, executed deliberately after the change committed. 3. **Restore from backup or point-in-time recovery** — rewinding the whole database, which loses every write since that point. The interesting judgement is about (2) versus forward-fixing, with (3) as the break-glass option. ## Why down migrations disappoint A down script can restore *structure*. It cannot restore *information*. - `ALTER TABLE orders DROP COLUMN legacy_ref` is reversible in shape (`ADD COLUMN legacy_ref`) and irreversible in content — every value is gone. - `UPDATE customers SET country = upper(country)` has no inverse: you cannot tell which values were already uppercase. - `ALTER TABLE t ALTER COLUMN amount TYPE NUMERIC(10,2)` may have truncated precision; widening it back does not restore the lost digits. Even for a genuinely reversible change, running the down script is only safe if nothing began depending on the new state in between. Once the deployed application writes into a new column, dropping it discards production data written by the current version — a data-loss incident dressed up as a recovery. And there is the maintenance argument: down scripts are written speculatively and executed almost never, so they rot. A rollback path exercised for the first time during an incident is exactly the code you do not want to trust. ## When rollback is genuinely fine The safe envelope is narrow but real: - The migration was **purely additive** — new table, new nullable column, new index, new non-validated constraint. - **No writes yet depend on it**, either because the code using it has not been enabled or because you can prove the new object is empty. - The window is **short**, minutes rather than days. - The rollback itself is cheap — dropping an index built over hours is instant, but you will pay for it again. A classic legitimate case: a new index degraded the plan for an important query, and dropping it immediately restores previous behaviour with zero data implication. ## Forward-fix as the default Forward-fixing means diagnosing what the schema now is and appending a migration that moves it to what it should be. Advantages: - The path is the same one every environment exercises constantly, so it is well tested. - The migration history stays a truthful, linear narrative — including the mistake and the correction — which is what you want during a post-mortem. - It composes with the application: you can ship a code fix and a schema fix as one ordinary deploy rather than an exceptional procedure. What makes forward-fix always available is designing changes so the *previous* application version still works against the *new* schema. Then a bad deploy is recovered by rolling back the **code** — fast, safe, well-understood — while the schema simply stays ahead. That is why teams separate additive schema changes from destructive ones and defer the destructive half by days or weeks. ## Making destructive changes recoverable When you truly must remove information, buy yourself an undo: - Copy first: `CREATE TABLE customer_legacy_ref_backup AS SELECT id, legacy_ref FROM customer` in the migration before the drop, and remove the backup table in a much later migration. - Rename instead of drop as the first step (`ALTER TABLE … RENAME COLUMN x TO x_deprecated`); reversal is a rename back, and the data survives. - For big data transformations, write the result into a new column and keep the original until you are confident. Each turns "irreversible" into "reversible for a bounded period", which is what actually makes an incident survivable. ## The break-glass option Point-in-time recovery is the last resort: it restores everything, including data you wanted to keep, and it costs downtime. It is the correct call for genuine corruption — a migration that mangled a table's contents across the board — and the wrong call for a merely inconvenient schema. Knowing your RPO/RTO and having actually rehearsed a restore is what makes this an option rather than a hope. ## What to say in an interview "Forward-fix by default; keep down scripts only for additive changes and treat them as convenience, not safety; make destructive steps deferrable and reversible by copying data first; and rely on rolling back the *application* rather than the schema, which is why the schema must stay compatible with the previous release."

  • Your service deploys a code change and a migration together, and the code is bad. What do you roll back?
    The code, not the schema — provided the schema change was additive and the previous application version still runs against it. That is precisely why additive-first is the rule: it keeps the fast, well-rehearsed recovery path (redeploy the previous image) available. Rolling back the schema instead would be slower, riskier, and could discard rows the new version already wrote.
  • Liquibase can auto-generate rollback statements for many changesets. Does that make rollback safe?
    It makes the structural reversal convenient for changes Liquibase can invert, such as creating a table or adding a column. It does not make it safe: auto-generated rollback still cannot restore dropped data, and it says nothing about whether the running application or data written since then depends on the new state. Treat generated rollback as a developer-environment convenience and judge production reversal on the data implications.
  • How do you decide between forward-fixing and a point-in-time restore during an incident?
    Ask whether the data is wrong or merely the schema. If rows have been corrupted broadly and no query can reconstruct the correct values, PITR may be the only path, accepting the loss of writes since the restore point and the downtime. If the schema is wrong but the data is intact or reconstructible, forward-fix, because a restore throws away every legitimate write made since the migration.

A down migration is like un-baking a cake: you can take the tin back off the shelf, but the eggs are not coming back out.

saying these in an interview costs you the question

  • "Every migration should have a down script" stated as an absolute, with no acknowledgement that dropped data cannot be restored
  • Confusing automatic transactional rollback of a failed migration with deliberately reversing a committed one
  • Running a down script after the new application version has already written rows into the new column
  • Assuming DDL always rolls back transactionally, which is false on MySQL and older Oracle
  • Treating point-in-time recovery as a cheap undo rather than an operation that discards recent writes

context