River
Y CombinatorBacked by Y Combinator
FREE TEMPLATE

Technical Migration Plan Template

Every rollback closes on a nameable event. This plan puts that window in hours beside each check's detection time, then reports which checks still fit.

Free download  ·  No account needed

Every migration plan has a rollback section. Almost none of them says when the rollback stops working, and it always stops working. The moment the new cluster takes its first write, the moment the old version's artifact is garbage collected, the moment a retention boundary passes. So on day three, when the problem surfaces, nobody has worked out whether going back is still possible. It usually is not, and the moment it stopped being possible had a name.

The reference tooling is blunt about this. Postgres' upgrade documentation ships a six-branch revert tree keyed on which mode ran and whether the new cluster was started, and two branches end in restoring from backup. Kubernetes says that once an old ReplicaSet is deleted you lose the ability to roll back to that revision, keeping ten by default. A stock blue-green rollout scales the previous version down after thirty seconds. None of that is unusual, and none of it appears in a migration plan.

So a rollback is a window, and every window is a number. This pack derives it from the ending event, then joins it to each validation check's detection time. Harlow Grid's worked plan has nine phases and fourteen checks. Seven checks can trigger their own rollback. The daily billing reconciliation needs a billing cycle, so it answers twenty-six hours after a cutover whose rollback lapses in eight. It was filed under cutover validation, where it had sat unexamined for a month.

The check that answers after the rollback has gone

Every window is derived from its ending event, and every coverage column is a join. Neither is typed in.

Sequencing and Rollback Register

Harlow Grid, a fictional energy retailer moving its metering platform onto managed Postgres 16 and Kubernetes. Rollback Ends When is an event; the hours follow from it.

PhaseWhat movesRollbackEnds whenWindowRehearsedOne-way
P1Target cluster stood up, no trafficDelete the clusterNothing, while no data has been writtenunboundedNot neededNo
P2Expand-only schema change on the targetDrop the added columns and indexesThe application starts writing them, at P6unbounded2026-09-16No
P3Logical replication, old primary to targetDrop the subscriptionNothing, while writes go to the old primaryunbounded2026-09-18No
P4New app deployed, preview service onlyScale the deployment to zeroNothing, while no production traffic reaches itunbounded2026-09-22No
P55 percent of read traffic to the targetRevert the traffic weightOld ReplicaSet scaled down after the promotion delay0.5h2026-09-24No
P6All reads, and the new columns start being writtenRevert the weight, stop writing the columnsOld ReplicaSet scaled down after the promotion delay0.5hNeverNo
P7Writes cut over to the targetReverse replication, target to old primaryThe reverse slot falls outside WAL retention8hNeverYes
P8Old application fleet decommissionedRedeploy the retained previous revisionReplicaSet garbage collected past a limit of 3unboundedNeverNo
P9Old Postgres 13 cluster droppedRestore from backup, measured at 6hBackup retention expires at 14 days336hNeverYes

Nine phases, ordered by rollback rather than by convenience: four unbounded first, then the two half-hour phases, then the one-way cutover. Four rollbacks have never been rehearsed and they are the last four, which is the ordinary pattern and runs exactly opposite to where the stakes are. P2 is the coupling nobody spots: its rollback expires because of P6, two weeks later, so the register names the ending event as a phase identifier rather than a date. P8's window is measured in deploys, not hours, because the revision history limit is 3.

Validation Checks

A baseline measured on the old system before anything moved, beside the threshold. Detection is hours until the verdict means anything, not how long the query runs.

CheckPhaseAssertsBaselineThresholdDetectionWindowCan gate it
V-01P3Every row present in the target, ten largest tables1,412,880,310 rows on the old primaryExact match per table0.5hunboundedYes
V-03P5Read latency p99 does not regressp99 214ms, 14 daysUnder 250ms0.25h0.5hYes
V-05P6Meter reads identical for a sampled 5,000 meters5,000 of 5,000 on the old primary5,000 of 5,0002h0.5hNo, 1.5h late
V-06P6The new columns are written on every insert path17 insert paths, enumerated17 of 171h0.5hNo, 0.5h late
V-07P7No write is lost during the cutoverSequence high-water marks, both clustersZero gap0.25h8hYes
V-08P7Daily billing totals agree between the systems4,180,442 kWh per day, 90 daysWithin 0.01 percent26h8hNo, 18h late
V-09P7Monthly invoice values agree, 2,000 accountsInvoices already issued, SeptemberWithin one penny744h8hNo, 736h late
V-11P8No client still resolves the old endpoint9 distinct client identities, 7 days of logsZero for 72 hours72hunboundedYes
V-12P6The system is stableNo baseline was measuredNone declared0.5hNo, nothing to compare
V-13P7Performance is acceptable under loadNo baseline was measuredNone declared8hNo, nothing to compare
V-14P9Nothing depends on the old clusterNo baseline was measuredNone declared336hNo, nothing to compare

Fourteen checks in the shipped sheet. Seven can trigger the rollback they exist to trigger. Four have a real baseline and answer after the window has closed. Three cannot fail at all. V-05 and V-06 both failed in rehearsal, at 4,997 of 5,000 and 15 of 17, which is how a rounding change and two batch jobs nobody had grepped for were found. Neither would have failed in time if it had first run at cutover. V-08 is the one that changed the plan: raising the old primary's write-ahead log retention from 8 hours to 48 brings it inside the window with 22 hours to spare, and costs 340GB on a machine being decommissioned anyway.

Risk Register

Every risk names one check and one rollback, both by identifier. Detectable in time is a join, not a judgement.

RiskPhaseSeverityDescriptionDetected byDetectionWindowIn time
R-05P7CriticalWrites lost between stopping the old path and starting the newV-070.25h8hYes
R-06P7CriticalBilling totals diverge because a rounding rule differsV-0826h8hNo, 18h too late
R-07P7CriticalInvoice values change for accounts with a mid-month tariff switchV-09744h8hNo, 736h too late
R-10P9CriticalSomething still depends on the old cluster after it is droppedV-14336hNo, the check has no baseline
R-03P6HighSampled reads disagree after a data type widenedV-052h0.5hNo, 1.5h too late
R-04P6HighAn insert path nobody enumerated skips the new columnsV-061h0.5hNo, 0.5h too late
R-09P8HighA client nobody knows about still calls the old endpointV-1172hunboundedYes
R-12P7HighThroughput collapses under a load pattern the test missedV-138hNo, the check has no baseline
R-01P3HighReplication falls behind and the target divergesV-021hunboundedYes
R-11P6MediumThe system misbehaves in a way nobody predictedV-120.5hNo, the check has no baseline

Twelve risks in the shipped sheet: five detectable while the rollback still exists, four detected only after it expired, three with no detection at all. Four risks are critical and three of them are not in the first group. Three of the four sit on P7, the eight-hour one-way phase. R-10 is the quietest: its check has no baseline, so a critical risk on the phase with the longest window in the plan currently has no detection except a repeated notice to everyone holding a credential on the old cluster, which is detection by outage.

What comes in the pack

01

Sequencing register with the ending event beside the window

One row per phase. Rollback Ends When names the event: the reverse slot falling outside write-ahead log retention, the old ReplicaSet being garbage collected past the revision limit, the promotion delay expiring, the backup ageing out. The hours follow from the event rather than the other way round, because the event can be changed and a guessed number cannot. Unbounded is a real value and four of the nine worked phases carry it, which is what lets attention go to the five that do not.

02

A rehearsed column, because an unrehearsed rollback is a paragraph

Rehearsing tells you two things nothing else will: whether the rollback works and how long it takes. The second is the number the window has to accommodate, and a six-hour rollback inside an eight-hour window is two hours of decision time rather than eight. Four of the nine worked rollbacks have a rehearsal date, one is recorded as not needed, and four have never been run. They are the last four, which is the ordinary pattern and runs exactly opposite to where the stakes are.

03

Validation Checks that are comparisons, not a checklist

Every row carries a baseline measured on the old system on real traffic before anything moved, with its source and date, beside the threshold and the reading. Eleven of fourteen worked checks have one. The three that do not read "the system is stable," "performance is acceptable under load" and "nothing depends on the old cluster," and none of them can fail. That outcome has a name in the reference tooling rather than being a rhetorical point.

04

The join, which is the whole reason detection time is a field

Detection time is hours from the phase landing to the check producing a verdict that means anything, not how long the query runs. Set it beside the phase's window and one column falls out: can this check trigger the rollback it exists to trigger. Seven of fourteen can. The monthly invoice comparison answers 736 hours after its rollback ended, which is thirty days and sixteen hours, and it is still a good check. It was never a gate.

05

Risk Register that inherits the join

Every risk names one check and one rollback by identifier, and the sheet reports whether detection lands while the rollback still exists. Five of twelve worked risks do. Four are detected only after it expired. Three have no detection at all, because their check has no baseline. Four risks are critical and three of those four are in the second or third group, which is a sentence nobody would sign off if it were on the front page, so it is on the front page.

06

A runbook, a per-phase rollback procedure and a comms plan keyed on the phase

The cutover runbook names one person who calls the rollback and decides the no-go readings in advance, because at 02:40 with a graph moving the wrong way nobody invents a threshold well. The rollback procedure has nine sections rather than one, since there is no such thing as the rollback for a migration. Every notice states the window in hours, which is the line that gets replies from integration partners. The decision to migrate at all belongs upstream, in an architecture decision record.

How it works

  1. 1

    Send the architecture and the config

    Current and target state, the dependency map, traffic data, a runbook draft. River also goes looking for the two things that set every window and nobody sends: retention settings, and the revision-history and scale-down configuration.

  2. 2

    Phases are cut by their rollback

    A phase is a change that can be undone as a unit, so the test is whether reverting half of it leaves a state nobody thought about. Then they are ordered with the unbounded rollbacks first and the one-way phases as late as the dependencies allow.

  3. 3

    Every window is derived from its ending event

    River reads the effective configuration rather than the documented default, names the event, and computes the hours. It also names the single change that would most extend the tightest window, which is usually one setting on a system being decommissioned anyway.

  4. 4

    Then the join tells you what you have

    Each check gets a baseline, a threshold and a detection time, and the coverage columns compute. You get the count of real gates, the risks nobody could catch in time, and the checks that cannot fail. Reconstruct an undocumented system first if you need to, with inherited architecture reconstruction.

Frequently asked questions

Why does a rollback need an expiry date?

Because it has one whether you write it down or not. Postgres' revert instructions branch on whether the new cluster was ever started, and if it was in link mode the old cluster is unsafe and has to come back from backup. Naming the event is what turns that from a surprise into a number.

How do you get the window in hours?

From the ending event, by reading the effective configuration rather than a default. Write-ahead log retention gives a reverse-replication window. Backup retention gives a restore window. A promotion delay gives a traffic-shift window, and the stock default is thirty seconds, which is worth knowing before you rely on it.

What if a rollback genuinely never expires?

Record it as unbounded, which is a real value, and never put a false clock on it. Four of the nine worked phases are unbounded: a cluster nobody has written to, an expand-only schema change with nothing reading the new columns, a deployment behind a preview service. Knowing which four is what lets you spend attention on the rest.

Why can't a check without a baseline fail?

Because there is nothing for the reading to contradict. "Performance is acceptable" admits no measurement that disproves it. Argo Rollouts evaluates against a success condition and a failure limit declared before the rollout, and names the outcome when neither resolves: Inconclusive. Three worked checks sit there before the migration has started.

What do I do about a check that answers too late?

Three honest responses. Extend the window, which is usually one retention setting on a system being decommissioned anyway. Shorten the detection by running the check as a shadow against both systems before cutover. Or accept in writing on the row that the recovery is a forward fix. What is not honest is leaving it under cutover validation.

Is a rollback window the same as an outage budget?

No, and conflating them is why the number goes missing. A window is how long the option to go back exists. How long you will tolerate being broken before using it is a separate and usually much shorter number. The rollback's own duration is a third. All three go on the row and they answer different questions.

Does this replace the design document for the migration?

No. Proposing the change belongs in an RFC register, and the target architecture belongs in a solution architecture document. This pack starts once the destination is agreed and the only remaining question is whether you can get back.

Find out which rollback you no longer have

Send the architecture and the config. River derives every window from its ending event, then reports which checks still fit.

Install the migration plan pack