Deploying a new application version often involves changing the database schema. You might add a column, rename a table, change a data type, or remove a field that the application no longer uses.
The deployment may work as expected at first, but a problem can appear later. An API might fail, a background job might stop processing records, or a performance issue might force you to restore the previous application version.
Rolling back the application is usually straightforward. Rolling back the database is more complicated, especially when the migration has already changed or removed data.
For example, if a migration drops a column and the application writes new records using the updated schema, restoring the old application alone will not bring the deleted column or its previous values back.
In this article, we will look at how to handle database migrations during rollbacks, how to use Entity Framework Core safely, and how to plan deployments so that an application can return to an earlier version without losing important data.
Why Rolling Back a Database Migration Is Different
An application deployment and a database migration are two separate operations.
The application deployment changes the code running on your servers. A database migration changes the schema or data stored in the database.
Suppose an application originally stores a customer's full name in one column:
CREATE TABLE dbo.Customers
(
Id INT PRIMARY KEY,
FullName NVARCHAR(200) NOT NULL
);
A new application version separates the name into two columns:
ALTER TABLE dbo.Customers
ADD FirstName NVARCHAR(100) NULL,
LastName NVARCHAR(100) NULL;
The new application can use FirstName and LastName. However, the old application still expects FullName.
If the FullName column remains available, the old application may continue working, provided its assumptions about the schema and data remain valid.
Now consider a migration that drops FullName:
ALTER TABLE dbo.Customers
DROP COLUMN FullName;
The previous application version can no longer run its original query successfully.
Even if you recreate FullName, the original values will not automatically return. You need a recovery strategy that accounts for the data that was removed and any changes made after deployment.
This is why database rollback planning must consider both schema compatibility and data preservation.
Step 1: Identify What the Migration Changed
Before rolling back a deployment, determine exactly what the migration changed.
Review the migration script and identify whether it:
Added or removed columns.
Renamed tables or columns.
Changed data types or nullability.
Added or removed indexes and constraints.
Modified existing data.
Deleted records or values.
Changed stored procedures, views, or other database objects.
Schema changes and data changes have different recovery requirements.
For example, removing an index can usually be reversed by creating the index again. Dropping a column containing customer data requires a way to recover that data.
If the migration transformed values, verify whether the transformation can be reversed without losing information.
Do not assume that every migration has a safe automatic rollback.
Step 2: Check the Current Database and Application Versions
Before making changes, confirm which application version is running and which migrations have been applied.
In an Entity Framework Core project, inspect the migration history:
SELECT
MigrationId,
ProductVersion
FROM dbo.__EFMigrationsHistory
ORDER BY MigrationId;
The table name and schema can vary if the application uses a custom migration-history configuration.
The migration history records migrations EF Core has applied. It does not prove that every manual database change has been recorded or that the database still matches the expected schema.
Also check:
Which application version is deployed.
Whether other instances are still running the previous version.
Whether background workers use the same database.
Whether the migration completed successfully.
Whether any writes occurred after the migration.
Whether a recent database backup is available.
These details determine whether a code-only rollback is sufficient or whether a database recovery operation is needed.
Step 3: Prefer Rolling Back the Application First
If the database remains compatible with the previous application version, rolling back the application may be the safest immediate response.
For example, suppose a migration adds a nullable column:
ALTER TABLE dbo.Customers
ADD PreferredLanguage NVARCHAR(20) NULL;
The old application may not use PreferredLanguage at all. If the new application has a defect unrelated to this column, the previous application version may continue working without reversing the migration.
In that situation, you can redeploy the previous application build while leaving the additional column in place.
However, test this assumption. The previous application must still work with the current schema, constraints, stored procedures, and data.
A code rollback is not automatically safe if the new version changed existing data or introduced database constraints that the old version cannot satisfy.
Step 4: Use the Expand-and-Contract Pattern
For important applications, a safer deployment strategy is to introduce schema changes gradually instead of making the database incompatible with the previous version immediately.
This approach is often called the expand-and-contract pattern.
Consider renaming FullName to DisplayName.
A risky migration might immediately rename the column:
EXEC sp_rename
'dbo.Customers.FullName',
'DisplayName',
'COLUMN';
The old application still expects FullName and may fail as soon as the migration completes.
A safer approach uses several deployments.
Phase 1: Expand the schema
Add the new column while keeping the old column:
ALTER TABLE dbo.Customers
ADD DisplayName NVARCHAR(200) NULL;
At this stage, both the old and new application versions can potentially work with the schema.
Phase 2: Backfill existing data
Copy the existing values into the new column:
UPDATE dbo.Customers
SET DisplayName = FullName
WHERE DisplayName IS NULL;
For a large table, perform the backfill in batches if needed to reduce transaction log pressure and limit the impact on normal application traffic.
Phase 3: Deploy compatible application code
Deploy code that can read the new column while maintaining compatibility with the previous version.
If both versions may write to the table during deployment, plan how updates to the old and new columns will remain consistent. A one-time backfill does not keep the columns synchronized after new writes.
Depending on the system, you may temporarily dual-write, use a controlled database mechanism, or deploy code in stages so that only one representation is authoritative at each step.
Phase 4: Stop using the old column
After all application instances and background workers have moved to the new representation, confirm that no active code still depends on FullName.
Only then should you remove the old column in a separate migration.
ALTER TABLE dbo.Customers
DROP COLUMN FullName;
This final step should happen only after the rollback window has closed and the team has verified that the old application version is no longer required.
The main benefit is that application and database changes do not need to happen as one irreversible operation.
Step 5: Understand EF Core Migration Rollbacks
Entity Framework Core can generate migrations that move a database backward to an earlier migration.
For example, you can generate a migration script that rolls the database back to a previous migration:
dotnet ef migrations script NewMigration PreviousMigration
Replace the migration names with the actual identifiers from your project.
The generated script contains the operations needed to move from the specified migration to the target migration, based on the migration definitions.
Review the output before running it. A migration's Down method may remove a column, drop a table, or reverse a transformation in a way that loses data.
For example, an automatically generated rollback might drop a newly added column. If the application has written important values into that column since deployment, those values will be lost when the rollback runs.
EF Core does not automatically reconstruct deleted data just because a migration has a Down method.
For production databases, generating and reviewing a script is generally safer than blindly running a database rollback command during an incident.
Step 6: Understand What Happens When a Column Is Dropped
Suppose a migration removes a column containing customer preferences:
ALTER TABLE dbo.Customers
DROP COLUMN PreferredLanguage;
A rollback could recreate the column:
ALTER TABLE dbo.Customers
ADD PreferredLanguage NVARCHAR(20) NULL;
But this only recreates an empty column. It does not restore the previous values.
To recover those values, you need another source, such as:
A database backup.
A point-in-time database restore.
An archived copy of the removed data.
A separate table that retained the original values.
An application-level event or audit log that contains sufficient information.
The right recovery method depends on the database platform, recovery configuration, retention period, and whether the recovery must preserve writes made after the migration.
For a destructive change, plan the data recovery strategy before deploying the migration.
Step 7: Use a Backup or Point-in-Time Restore When Necessary
If a migration has deleted important data, restoring from a backup may be necessary.
However, restoring the entire production database to an earlier point can discard legitimate transactions that occurred after that point.
For example, if you restore the database to a time before the migration, you may recover the removed column values but also lose customer orders created afterward.
A safer recovery process may involve restoring a backup to a separate database, extracting the missing data, and merging it into the current production database.
A typical recovery plan is:
Stop or limit the affected writes if necessary.
Preserve the current database state for investigation.
Restore a suitable backup to a separate environment.
Extract the required data.
Validate the extracted records and their relationships.
Merge the recovered data into the active database.
Verify the application and database consistency.
Resume normal operations after validation.
The exact process depends on your database engine and recovery model. For SQL Server, point-in-time recovery depends on having the required backups and transaction log backups available.
Always test restore procedures before an incident. A backup is useful only if it can be restored successfully and within the required recovery time.
Step 8: Test Rollback Compatibility Before Deployment
A migration should be tested against both the new application version and the previous version when rollback is part of the deployment plan.
A useful deployment test sequence is:
Restore a representative copy of the database.
Deploy the current application version.
Apply the proposed migration.
Run integration tests against the new schema.
Deploy the previous application version.
Verify that its queries and writes still work.
Check whether the previous version can handle data written by the new version.
Confirm the recovery process for destructive changes.
The sixth and seventh steps are important. Even when the schema remains compatible, new application code might write values that the old code does not recognize.
For example, a new version may introduce a new status value. The old version might fail when it encounters that value, even though no columns were removed.
Schema compatibility includes data compatibility, not just the presence of database objects.
Common Mistakes to Avoid
Assuming every migration can be reversed safely
A Down migration may reverse schema changes but cannot necessarily restore deleted or overwritten data.
Running a rollback without reviewing the script
Review the generated SQL and identify destructive operations before applying it to a production database.
Removing old columns too early
Keep old columns and compatibility logic until the previous application version is no longer needed and the rollback window has closed.
Ignoring background workers
An API may be rolled back while a background worker continues running newer code against the same database. Include all database clients in the deployment plan.
Restoring the entire database without considering new transactions
An older restore point can recover lost data but may also discard legitimate changes made afterward. Evaluate whether a selective recovery is possible.
Assuming a recreated column restores its values
Adding a column only recreates the schema. Data must be recovered from a source that still contains it.
Best Practices for Production Applications
Prefer backward-compatible database changes during application deployments.
Use the expand-and-contract pattern for important schema changes.
Generate and review migration scripts before applying them.
Keep tested backups and verify that restore procedures work.
Define a rollback plan before deploying destructive migrations.
Test the previous application version against the migrated database.
Consider data compatibility when adding new enum or status values.
Coordinate migrations across APIs, background workers, and scheduled jobs.
Avoid dropping old columns until the rollback window has closed.
Document the decision points for code rollback, migration rollback, and data recovery.
Summary
Rolling back an application after a database migration requires more than redeploying an earlier build. The previous application must be compatible with the current database schema and the data stored in it.
When possible, roll back the application without reversing a backward-compatible database migration. For longer-term changes, use the expand-and-contract pattern so that old and new application versions can coexist during deployment.
If a migration has removed or overwritten important data, a Down migration may not be enough. You may need a backup, point-in-time restore, or a controlled data-recovery process.
The safest rollback is one that has been planned and tested before deployment, rather than improvised after production starts failing.
Join the conversation! Your thoughts help the community grow.