Data Migration Strategies
Plan and execute safe database migrations in production.
By EMEPublished: February 20, 2025
databasemigrationdataschema changeproduction
A Simple Analogy
Data migration is like moving a family to a new house. Plan carefully, pack methodically, verify everything works, and keep backup of what you had—just in case.
Migration Patterns
| Pattern | Risk | Time | Use Case | |---------|------|------|----------| | Big Bang | High | Fast | Development | | Blue-Green | Low | Slow | Production | | Rolling | Medium | Medium | Large systems | | Shadow | Low | Slow | Validation |
Blue-Green Deployment
// Keep two identical databases: Blue (old) and Green (new)
// Switch traffic after validation
var activeDatabase = await GetActiveDatabaseAsync();
if (activeDatabase == "blue")
{
// Migrate data to green
await MigrateDataAsync("blue", "green");
// Validate green
if (await ValidateAsync("green"))
{
// Switch traffic to green
await UpdateConfigAsync("activeDatabase", "green");
}
else
{
// Rollback to blue
await UpdateConfigAsync("activeDatabase", "blue");
}
}
Rolling Migration
// Migrate in batches to minimize downtime
public async Task MigrateInBatchesAsync(
IEnumerable<int> userId,
int batchSize = 1000)
{
var batches = userId
.Chunk(batchSize)
.ToList();
foreach (var batch in batches)
{
try
{
await MigrateBatchAsync(batch);
logger.LogInformation($"Migrated batch of {batchSize}");
}
catch (Exception ex)
{
logger.LogError($"Batch failed: {ex.Message}");
await RollbackBatchAsync(batch);
}
}
}
Schema Changes
-- Add column (backwards compatible)
ALTER TABLE Users ADD Email NVARCHAR(255) NULL;
-- Remove column (requires app update first)
-- Step 1: App stops using column
-- Step 2: Migrate data
ALTER TABLE Users DROP COLUMN LegacyField;
-- Rename with safe approach
-- Step 1: Add new column
ALTER TABLE Users ADD NewColumnName INT;
-- Step 2: Copy data
UPDATE Users SET NewColumnName = OldColumnName;
-- Step 3: Update app to use new column
-- Step 4: Drop old column
ALTER TABLE Users DROP COLUMN OldColumnName;
Validation Strategy
public class MigrationValidator
{
public async Task<bool> ValidateAsync(string sourceDb, string targetDb)
{
// Count validation
var sourceCount = await CountRecordsAsync(sourceDb);
var targetCount = await CountRecordsAsync(targetDb);
if (sourceCount != targetCount)
return false;
// Data integrity check
var issues = await CheckDataIntegrityAsync(targetDb);
if (issues.Any())
{
logger.LogError($"Found {issues.Count} integrity issues");
return false;
}
// Performance check
var queryTime = await MeasureQueryPerformanceAsync(targetDb);
if (queryTime > TimeSpan.FromSeconds(5))
{
logger.LogWarning("Query performance degraded");
// Decide if acceptable
}
return true;
}
}
Rollback Plan
// Always have rollback procedure ready
public async Task RollbackAsync(string targetDb)
{
logger.LogInformation("Starting rollback...");
// Switch traffic back to source
await UpdateConfigAsync("activeDatabase", "source");
// Verify working
var healthy = await HealthCheckAsync("source");
if (!healthy)
{
logger.LogCritical("Rollback failed - manual intervention needed");
await AlertOpsTeamAsync();
}
// Clean up target database
await DropDatabaseAsync(targetDb);
logger.LogInformation("Rollback complete");
}
Best Practices
- Test extensively: Dry-run before production
- Have rollback plan: Always prepared to revert
- Validate thoroughly: Check counts, integrity, performance
- Communicate clearly: Notify stakeholders
- Monitor closely: Watch for issues during migration
Related Concepts
- Database backups and recovery
- Transaction logs and point-in-time recovery
- Feature flags for feature control
- Monitoring and alerting
Summary
Safe migrations require planning (blue-green or rolling), validation, and rollback capabilities. Test thoroughly before production to prevent data loss and service interruption.