Zero-Downtime Database Changes: Expand-Contract Pattern for Microservices
Why DB changes break safe deployment
Application rollback is easy only when the old version can still understand the database.
Bad migration:
ALTER TABLE customer
DROP COLUMN full_name;
ALTER TABLE customer
ADD COLUMN first_name VARCHAR(100) NOT NULL,
ADD COLUMN last_name VARCHAR(100) NOT NULL;If old pods still expect full_name, rolling/canary deployment breaks.
I use expand-contract.
Phase 1: Expand
Add new structures without removing old ones.
ALTER TABLE customer
ADD COLUMN first_name VARCHAR(100) NULL,
ADD COLUMN last_name VARCHAR(100) NULL;Old code continues working.
Phase 2: Compatible application
New code can read new fields when present and fall back to old.
function displayName(
row: CustomerRow,
) {
if (
row.firstName ||
row.lastName
) {
return [
row.firstName,
row.lastName,
].filter(Boolean).join(' ');
}
return row.fullName;
}Writes may temporarily update both representations:
UPDATE customer
SET
full_name = ?,
first_name = ?,
last_name = ?
WHERE id = ?;I keep dual-write windows short because they increase complexity.
Phase 3: Backfill
UPDATE customer
SET
first_name = SUBSTRING_INDEX(
full_name, ' ', 1
),
last_name = NULLIF(
TRIM(
SUBSTRING(
full_name,
LENGTH(
SUBSTRING_INDEX(
full_name, ' ', 1
)
) + 1
)
),
''
)
WHERE first_name IS NULL;For millions of rows I do this in small batches to avoid long locks/replication lag.
Phase 4: Switch reads
After verifying:
new column populated
new app deployed everywhere
metrics cleanthe application reads only new fields.
Phase 5: Contract
In a later release:
ALTER TABLE customer
DROP COLUMN full_name;I do not combine this cleanup with the first deployment.
Index creation
Large index creation can lock or load production.
I use database-specific online/concurrent mechanisms where available and monitor:
lock waits
replication lag
IO
CPU
query latencyRename column
Rather than immediate rename:
add new column
write both
backfill
read new
remove oldThis supports mixed app versions.
Enum/status evolution
Never deploy code that writes a new enum value old code cannot parse if rollback is still expected.
I use additive tolerant parsing first.
Production checklist
- Add before remove.
- Keep new columns nullable initially when needed.
- Deploy tolerant code.
- Backfill in batches.
- Monitor locks/replication.
- Verify old/new values.
- Switch reads.
- Stop dual writes.
- Wait through rollback window.
- Remove old schema later.
FAQ
Can blue-green avoid schema compatibility?
No. Blue and green frequently share the database.
How long should expand-contract take?
Often multiple releases. Safety is more important than compressing every step into one pipeline run.
Should DB migration auto-rollback?
Destructive rollback is risky. I prefer forward-compatible migrations and roll-forward fixes.
Related architecture guides
- [Production Deployment Patterns I Have Used](/articles/production-deployment-patterns-blue-green-canary-rolling)
- [Rolling Deployment](/articles/rolling-deployment-kubernetes-production)
-
Architect's final take
Most zero-downtime deployment problems become database-contract problems. I treat schema compatibility as part of the release architecture, not a DBA step after coding.
Your feedback helps prioritize deeper technical content.




