Imagine a small catalog service that stores an item’s visible label in a field called name. A cleanup release renames it to display_label, updates the application, and removes the old field. The new release passes its tests. Later, an unrelated search bug prompts the operator to restore the previous application.

The old program starts, but its queries still ask for name. The rollback restored the executable, not the data contract. A release directory was available; a usable return path was not.

This is an illustrative migration, not a production incident. The field’s meaning stays exactly the same throughout: one label string. Changes in meaning, validation rules, or allowed values would need additional compatibility decisions.

Name the return point before changing state

A rollback plan should identify a specific application version and the state it can safely handle. “Use the previous build” is incomplete if that build expects a different schema, configuration, or queued-message format.

Amazon’s Ensuring rollback safety during deployments describes compatibility in both directions: new code must handle existing data, and the intended rollback version must handle data produced during the new release. Mixed-version deployments add another case because both versions may be active together.

For the catalog, call the original application R1. It reads and writes only name. Keeping that field present is necessary for R1, but not sufficient for every future transition. If R1 changes a label while another field holds a copy, that copy can become stale. Write behavior belongs in the plan alongside read behavior.

Build a bridge release with one source of truth

Danilo Sato’s Parallel Change describes an expand, migrate, and contract approach: introduce the new interface alongside the old one, move consumers, then remove the obsolete interface. For this catalog, the following staged plan makes the transition explicit.

R1 · Original
Reads and writes name. The new field is not required.
R2 · Bridge
After adding display_label, still reads name; writes both fields together with the same value.
Migration gate
Remove all R1 writers, synchronize existing rows, and verify that both fields agree.
R3 · New reader
Reads display_label and continues writing both fields. R2 is the designated rollback target.
Later cleanup
Remove the legacy field only after retiring every supported version that needs it.

While R1 and R2 coexist, name remains authoritative. Do not switch readers merely because most rows have a populated new field: an R1 writer may have changed the old field since it was copied.

In this example both values live in one database row, so require each R2 or R3 edit to update them in the same transaction. Two independent writes are not equivalent. If the fields lived in separate systems, this plan would need a separate consistency and recovery mechanism; calling both operations does not make them atomic.

Backfill without overwriting a newer edit

Suppose a migration worker reads the label “Desk lamp.” Before it writes the copy, an editor changes the item to “Reading lamp.” A blind update from the worker can put the older value into display_label.

Use a concurrency-safe operation appropriate to the database. One option is to condition the update on an unchanged row revision and retry from current state if the condition fails. Every relevant writer must advance that revision for the guard to mean anything. Another option is a correctly isolated transaction with suitable locking. Test the chosen mechanism rather than assuming a read followed by a write is protected.

Work in bounded batches and preserve progress so an interrupted migration can continue. A restart must not overwrite valid current values with an earlier snapshot. Once R1 writers are gone, reconcile mismatches as well as missing values; filling only empty fields leaves stale copies behind.

The gate to R3 is an observable condition: no legacy-only writers remain, all required rows are synchronized, and ongoing writes preserve agreement. Adding a column or a constraint may itself have operational costs. Check the chosen database’s behavior for the actual schema and workload before treating the example as an execution recipe.

Test rollback after the new version has written

A test that starts R3 and immediately replaces it with R2 barely exercises the dangerous part. Create and edit records through R3 first. Then roll back to R2 without resetting the database, read those records, edit them again, and verify that a subsequent upgrade still sees the latest values.

Also test R2 and R3 concurrently if production can run them together. Cover creates, updates, deletes, empty labels, and the allowed character set. The assertion is not just “the request returned successfully”; it is that the intended label survives each transition.

Include background importers, scheduled jobs, and administrative tools in the writer inventory. A quiet R1 process can invalidate the migration gate even if every web instance runs R2. Delayed jobs also need attention: old queued work can outlive the release that created it.

Test the old version against the state left by the new version. Restoring a clean fixture before rollback removes the condition you need to examine.

Close the compatibility window deliberately

Under this plan, R3 can return to R2 while both fields remain synchronized. R1 is no longer the approved target: it can write only the old field. An emergency return to R1 would require a new reconciliation and verification step before enabling new-field readers again.

Keep that boundary visible in the release record. Before removing name, identify the minimum supported application version, check remaining readers and writers, and retire recovery procedures that still assume R2 can run. Remove the old field in a separately reviewed change, not as an invisible final step of the first rollout.

A database snapshot serves a different purpose. Restoring it can discard valid changes accepted since the snapshot. If restoration becomes necessary, plan how to preserve or reconcile those changes. An application rollback and a data restore should not share a button merely because both move something backward.

Keep the recovery record short and specific

For the R3 release, a useful handoff names R2 as the return target, the compatible schema, the synchronization checks, and the observation that would trigger rollback. It also says which changes are outside that promise.

  • Target: retain the exact tested R2 artifact and required configuration.
  • State: keep both fields present and verify their agreement.
  • Writers: confirm that no legacy-only writer can silently return.
  • Exercise: test R3 writes, R2 rollback, and a later upgrade on the same data.
  • Boundary: name the cleanup change that ends support for R2.
  • Alternative: document repair or restoration when ordinary rollback is no longer safe.

A retained executable is a useful artifact. A rollback capability is a tested relationship between that artifact and the state it will encounter. Preserve both until the compatibility window is deliberately closed.