A deployment can roll back cleanly while the database stays incompatible with the old application. Before shipping a schema change, check whether the previous release can still read and write the data that the new release creates.
Consider a fictional account service. Version 1 reads a column called display_name. Version 2 expects public_name. Renaming the column and deploying the new code together looks tidy, until an old worker, a rolling-deployment instance, or an emergency rollback still asks for display_name.
The migration succeeded. The service can still fail.
Separate compatibility from cleanup
Danilo Sato’s explanation of parallel change describes three stages: expand, migrate, and contract. The useful idea is to introduce the new interface while the old one remains available, move consumers, then remove the old interface.
For a database rollout, turn that idea into explicit release gates:
- Expand: introduce the new storage shape without removing what the currently deployed application needs.
- Migrate: move reads and writes deliberately, checking compatibility during the transition.
- Contract: remove the old shape only after its consumers and the intended rollback window are accounted for.
These stages are a planning model, not a guarantee of zero downtime. The exact migration still depends on the database, data volume, application behavior, and operational limits.
Work through the awkward middle
In the account-service example, adding public_name is only the beginning. Before copying values, decide which field is authoritative at each stage.
A possible transition is to deploy compatibility code that continues serving old readers while maintaining both values. If both columns live in the same row, update them in the same database transaction where practical. That still leaves an important question: can any old application instance or background job write only the original column?
Do not switch reads to the new column while those writers can silently make it stale. Either finish upgrading writers first or design a deliberate synchronization mechanism. A fallback read helps with missing values; it does not detect an outdated non-empty value.
Backfills need the same care. A naive copy can overwrite a newer value if it races with a live update. Define an update condition, concurrency strategy, and restart behavior before running the job. Record progress so an interrupted backfill can resume without blindly replaying the entire table.
Test the rollback with new data
A useful rehearsal includes more than deploying version 2 successfully:
- Start with representative data, including empty and unusual values.
- Apply the expansion and deploy the compatibility release.
- Run the backfill alongside representative writes.
- Create and edit records using the new release.
- Restore the application version you actually intend to roll back to.
- Verify that it can read and update those new records correctly.
Include background workers, scheduled jobs, exports, and direct database consumers in that exercise. A healthy web endpoint does not prove that every writer is compatible.
Use synthetic or appropriately protected test data. This checklist proposes evidence to collect; it does not claim that a production migration has been tested.
A compatible change can still block traffic
In PostgreSQL, ALTER TABLE generally acquires an ACCESS EXCLUSIVE lock unless the documentation specifies otherwise. Compatibility at the application level does not eliminate lock waits.
PostgreSQL also supports separating certain constraint additions from validation with NOT VALID and VALIDATE CONSTRAINT. The permitted constraint types and locking details matter. Check the documentation for your deployed version rather than applying the technique to every schema operation. See the PostgreSQL ALTER TABLE reference.
Before execution, define acceptable lock wait, query latency, replication lag, and backfill load. Decide who can stop the rollout and what the safe next step is if a threshold is crossed.
Make cleanup its own decision
Keep a short release note that answers four questions:
- Which application versions can run against this schema?
- Which data changes can the previous version understand?
- What evidence allows the old column or interface to be removed?
- After removal, does recovery mean application rollback, a forward fix, or data restoration?
Give the cleanup an owner. Leaving parallel paths indefinitely creates its own maintenance cost, but removing them too early destroys the compatibility you built.
The practical takeaway: rehearse rollback against data written by the new release before treating it as a recovery plan.
What is the most easily missed database consumer in your deployment process?
Waqas Ahmad | Software Engineer | Content Creator