When Your Database Migration Needs a Clean Break: The Cutover Phase
Imagine this scenario: your team has spent weeks carefully migrating data from an old database schema to a new one. The expand-contract pattern has been running smoothly. Your application has been writing to both the old and new structures. Backfill scripts have moved historical data. Verification checks have passed. Everything looks good on paper.
But now comes the moment that makes many engineers nervous: switching your application to read exclusively from the new structure. This is the cutover phase, and it is where many database migrations either succeed cleanly or create unexpected production incidents.
What Cutover Actually Means
Cutover is the point where your application stops reading from the old structure and relies entirely on the new one. It sounds simple, but in practice it requires careful coordination and verification.
Before cutover, your application has been in a dual-read state. New data has been written to both structures since the migration started. Historical data has been backfilled. The application has been reading from two places: the old structure for data that existed before the migration, and the new structure for data written afterward.
Cutover removes that dual-read logic. Your application code changes so that every read request goes only to the new structure. This is typically a code change and deployment, not a schema change. You update the read path, build the application, and deploy it like any other feature update.
The following sequence diagram illustrates the transition from dual-read to cutover:
Here is a simplified example of what that code change looks like in a Node.js service:
// Before cutover: dual-read logic
async function getUserProfile(userId) {
// Try new structure first for recent data
const newProfile = await db.query(
'SELECT * FROM user_profiles_v2 WHERE user_id = $1', [userId]
);
if (newProfile.rows.length > 0) {
return newProfile.rows[0];
}
// Fall back to old structure for legacy data
const oldProfile = await db.query(
'SELECT * FROM user_profiles WHERE user_id = $1', [userId]
);
return oldProfile.rows[0] || null;
}
// After cutover: single-read logic
async function getUserProfile(userId) {
const profile = await db.query(
'SELECT * FROM user_profiles_v2 WHERE user_id = $1', [userId]
);
return profile.rows[0] || null;
}
The Risk You Cannot Ignore
The danger during cutover is partial failure. If some application instances still read from the old structure while others have switched, you can get inconsistent results. A user might see different data depending on which server handles their request.
This is why cutover strategy matters. You have two main approaches:
Big bang cutover switches all instances at once. It is fast and simple to coordinate, but if something goes wrong, every user is affected immediately.
Gradual cutover switches instances in batches, often by region, availability zone, or user group. This limits the blast radius. If you cut over one availability zone and see errors, you can investigate before proceeding to the next zone. The trade-off is more complexity in coordination and monitoring.
Most teams with production experience prefer gradual cutover for database migrations. The extra coordination effort is worth the safety net.
Finding Hidden Dependencies
After cutover, your application no longer reads from the old structure. But is it really the only application that does? In production environments, multiple services, batch jobs, reporting scripts, and manual queries often share the same database.
A common mistake is assuming that updating your main application is enough. Meanwhile, a nightly batch job that runs at 2 AM still queries the old column. Or a data analyst runs a manual report every Monday using the old table. These hidden dependencies will cause failures or produce stale data after cutover.
The most reliable way to find them is database query monitoring. Enable tools like pg_stat_statements in PostgreSQL or performance_schema in MySQL. Look for any queries that reference the old columns or tables. Run this monitoring for at least one full cycle of all known processes. If you have a weekly reporting job, wait a full week after cutover before declaring the old structure unused.
Some teams also run a staging test where they revoke read access to the old columns and run all known application scenarios. If no errors appear, production is likely safe for the next step.
What Happens After Cutover
Once cutover is complete and all dependencies are confirmed clean, your application is fully on the new format. The old structure still exists in the database, but nothing reads from it. This is the point where you can start planning the contract phase: removing the old structure entirely.
But do not rush into deletion. Even after cutover, keep the old structure for a safety period. The length depends on your team's confidence and the cost of keeping extra columns or tables. Some teams keep them for a week. Others keep them for a month, especially if the migration involves critical customer data.
During this period, monitor for any alerts or error logs that might indicate an undiscovered dependency. If nothing surfaces, you can proceed with confidence to drop the old structure.
Practical Checklist for Cutover
Before you execute cutover in production, run through this checklist:
- All historical data has been backfilled and verified
- Dual-write has been running without errors for at least one full business cycle
- Read path code change is ready and reviewed
- Rollback plan is documented: how to switch reads back to old structure if needed
- Database query monitoring is enabled and configured to catch old-structure queries
- All known dependent applications, batch jobs, and reports have been identified and updated
- Staging environment has been tested with old-structure read access removed
- Gradual cutover plan is defined: which instances or regions switch first
- Monitoring dashboards are set up to detect read errors or data inconsistencies after cutover
- Communication plan is ready: who needs to know about the cutover and when
The Takeaway
Cutover is the moment when your migration pattern stops being theoretical and starts affecting real users. Do not treat it as a simple code change. Treat it as a production event that requires monitoring, verification, and a clear rollback path. The extra day you spend confirming that no hidden dependencies exist is worth far more than the hour you might save by rushing to delete the old structure.