In this lesson, you will learn how to evolve your database schema without breaking background consumers that are still running older versions of your application code.
The Challenge of Asynchronous Version Mismatch
When you deploy a new version of a background worker, you rarely achieve a "big bang" update across all nodes simultaneously. If you alter a database column—such as renaming a field or changing a data type—a "v1" worker might still be polling a queue for messages that expect the old schema, while a "v2" worker is already writing data based on the new schema. This version skew leads to runtime exceptions and message processing failures.
The "Expand-Contract" Pattern
To handle this, we employ the Expand-Contract (or Parallel Change) pattern. Instead of a single destructive migration, you break the database change into three distinct phases:
- Expand: Modify the database to support both old and new requirements. If you are renaming
UserEmailtoContactAddress, you add the new column while keeping the old one. - Migrate: Update the application code to write to both columns, but read from the old one until all nodes are updated. Once all consumers support the new schema, switch them to read from the new column.
- Contract: After verifying the system is stable, drop the legacy column.
Implementation Illustration
In your C# consumer, you can handle the transition period using conditional logic:
public async Task ProcessMessage(UserMessage message)
{
// Reading from the new column if available, falling back to legacy
var email = await _db.Users
.Select(u => u.ContactAddress ?? u.UserEmail)
.FirstOrDefaultAsync(u => u.Id == message.Id);
// Business logic using the unified 'email' variable
await _processor.SendNotification(email);
}
By decoupling the database deployment from the application deployment, you ensure that even if a legacy worker processes a message, it can still interact with the database safely. This prevents "poison" messages from occurring due to schema mismatches, ensuring your background infrastructure remains robust during transition windows.
Key Takeaway: Never perform a destructive database migration in asynchronous systems; instead, use the Expand-Contract pattern to keep your old and new code paths compatible with the schema simultaneously.