A live SaaS product accumulates records created by many versions of its code. A schema change must therefore work with old data, new writes, and sometimes old and new application processes running together. A migration that succeeds on an empty database can fail in production if it assumes every record has the latest fields. Safe evolution uses small, compatible stages and a way to observe and resume work.
Begin with a compatibility plan
Describe the current shape, the desired shape, and which readers and writers use each field. Additive changes are easier to roll out: introduce a new optional field, deploy readers that handle its absence, then begin writing it. A later backfill can update old records, and only after every reader has changed should the old field be removed. For a renamed field, a temporary dual-read or dual-write period may be necessary. Decide how long overlapping application versions can run and what a rollback would do after new data has been written.
Make backfills bounded and restartable
A migration should process records in batches with a stable cursor or marker. Record progress and errors, and allow safe retries. Avoid locking an entire large collection during peak traffic. Test with data resembling the largest tenant and with malformed historical records, not just current examples. If tenant-specific settings change the field meaning, the backfill needs the correct tenant context. A dry run or count can estimate work before modification. Keep a backup and a clear stop condition for unexpected results.
Protect business meaning
A data conversion can change more than storage. Converting dates, currencies, statuses, or identifiers needs a domain rule, not a mechanical string replacement. Preserve historical transaction meaning and audit what was transformed. If some records cannot be converted automatically, put them in a review queue rather than silently guessing. The API should continue to return a coherent contract to clients while storage evolves. Use centralized ID utilities for identifier changes, since parsing or constructing IDs in a migration script can violate platform conventions.
Coordinate deployment and indexing
New queries may need indexes before new code begins using them. Build and verify indexes with an understanding of write load and storage. Deploy compatible code, run the backfill, monitor progress, then tighten validation when data is ready. Background jobs and rarely used routes may still read the old shape; include them in the inventory. A migration is complete when no supported process depends on the old representation and monitoring shows the new path working, not merely when the script prints “done.”
Plan recovery
A failed migration may be stopped and resumed, rolled back, or corrected forward. Choose based on whether new writes have used the new shape. Keep the previous build and data backup, but do not assume rolling back code alone restores old data semantics. Test the recovery path in a safe environment. Record the exact migration version applied to each deployment or tenant when data is separated. For a modest SaaS product, a documented manual gate may be appropriate for risky data changes even if ordinary code releases are automated.
Verify customer workflows
After the change, inspect representative records and run the operations customers use: lists, edits, exports, search, and background jobs. Compare counts and key values before and after. Watch error rates and slow queries. If a migration affects one tenant differently, check that tenant directly rather than relying on global averages. Schema evolution is successful when customers continue working, operators can explain the state of the data, and the team can make the next change without carrying an indefinite compatibility branch. Assign a date to remove transitional reads and writes once the data is verified. Old compatibility paths can hide future defects if left indefinitely. Keep the migration record with the deployment history so a later incident can identify exactly which schema steps ran.