A release introduces an application bug. You revert the deployment configuration, and the previous container image starts successfully.
But requests still fail.
The application has returned to its previous version. The database has not.
For stateful applications, a deployment rollback and a recovery plan cover different things.
A simple column rename can break rollback
Consider an application that reads this column:
SELECT customer_name FROM orders;
A new release renames it:
ALTER TABLE orders
RENAME COLUMN customer_name TO billing_name;
The new application expects billing_name. The previous application expects customer_name.
If the release fails after the migration, deploying the previous image leaves it querying a column that no longer exists.
The same compatibility problem can appear during a rolling deployment, while old and new application instances run together.
This is an illustrative example. The locking and operational impact of schema changes depend on the database engine and workload.
Separate schema evolution into stages
The expand, migrate, contract pattern spreads a breaking change across compatible steps. GitLab documents this approach for changes that must work across application versions. GitLab's compatibility guide
For this example, a staged approach could look like:
1. Expand
Introduce the new column while keeping the existing one.
Old application instances can continue using the original schema.
Adding a column still needs operational review: defaults, constraints, table size and database behavior can affect deployment safety.
2. Migrate
Introduce application behavior that supports the transition.
Define how both representations stay consistent while older instances remain active. A one-time backfill is insufficient if ongoing writes can immediately make the columns disagree.
Backfill existing records in controlled batches where appropriate. Make progress observable and retries safe.
Check completeness and consistency before switching reads.
3. Contract
Remove the old representation only after its consumers have stopped using it and the intended rollback window has closed.
Consumers may include background workers, reports, integrations and scheduled jobs—not just web application instances.
Once the old column is removed, an older application version may no longer be a valid recovery target.
Keep GitOps reconciliation in the recovery plan
A manual change to a running Deployment can conflict with the desired state maintained by a GitOps controller.
Decide how recovery changes the authoritative configuration, and how reconciliation behaves during the incident.
Also distinguish a Git revert from the deployment tool's rollback operation. They are separate mechanisms with their own behavior and constraints. Check the documentation for your deployed version, such as Argo CD's automated sync policy.
Neither operation inherently reverses database writes or external side effects.
Test the previous application against the migrated database
A useful compatibility test is:
Create representative starting data
→ Apply the proposed migration
→ Run the previous application version
→ Exercise important reads and writes
→ Run the new application version
→ Verify transition behavior
For a rolling release, also test the period when both versions operate concurrently.
A successful migration command proves that the database accepted the change. It does not prove application compatibility.
Write down where rollback stops being safe
Before release, answer:
- Which application versions support the resulting schema?
- Can migration jobs resume safely after interruption?
- What happens to writes made during the transition?
- At what step does recovery require a forward fix?
- If restoration is necessary, what happens to data written since the backup?
The recovery target is a compatible application-and-data state.
How does your team define the rollback window for releases that change the database?
Top comments (0)