{"post":{"slug":"expand-contract-migrations-2026","title":"Expand and Contract: Schema Changes Without Downtime","excerpt":"Renaming a column in one deploy breaks every running instance of the old code. Splitting the change into three deploys makes it invisible to users.","category":"code","date":"August 13, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">Y</span>ou need to rename `user_name` to `username`. The migration is one line. You deploy it alongside the code change and, for the ninety seconds your rolling deploy takes, half your instances are running old code querying a column that no longer exists.\n\nThe window is short. It is also exactly when every request against those instances fails. And rolling back is worse, because now the new code is querying a column you just renamed away.\n\nThe fix is not a better migration tool. It is accepting that old and new code coexist during every deploy, and designing the change so both work.\n\n## What changed in 2026\n\n- **Rolling deploys became universal.** Container orchestration made gradual rollout the default, which made the coexistence window unavoidable rather than exceptional.\n- **Migration tooling grew safety checks.** More frameworks warn on operations known to lock or break compatibility.\n- **Online DDL improved.** Databases got better at adding columns and building indexes without long locks, removing one class of problem while leaving the compatibility one.\n- **The pattern became standard practice.** Expand-contract stopped being an advanced technique and became how competent teams do schema changes.\n\n## The three phases\n\n**Expand.** Add the new structure without removing the old. Add the new column, nullable, with no constraint that existing writes would violate. Deploy code that writes to *both* columns and reads from the old one. Nothing breaks because the old column is untouched and the new one is optional.\n\n**Migrate.** Backfill the new column from the old for existing rows, in batches so you do not hold a long transaction — see [MVCC explained](/blog/mvcc-explained-2026) for why a long transaction is its own problem. Then deploy code that reads from the new column while still writing both. At this point the old column is written and unread.\n\n**Contract.** Once you are confident nothing reads or writes the old column, deploy code that only uses the new one, then drop the old column in a final migration.\n\n| Phase | Schema | Code writes | Code reads |\n| --- | --- | --- | --- |\n| Start | old | old | old |\n| Expand | old + new | both | old |\n| Migrate | old + new | both | new |\n| Contract | new | new | new |\n\nAt every point between phases, both the previous and current version of the code work against the current schema. That is the property that makes rolling deploys safe, and it is the whole design goal.\n\n## Each phase is a separate deploy\n\nThe most common way this goes wrong is compressing it. Adding the column and switching reads in one deploy means, during the rollout, some instances read a column that has not been backfilled yet.\n\nThree deploys, each fully rolled out and verified before the next begins. It is slower and it is the point — the safety comes from the gaps, not the phases.\n\nThe same reasoning applies to rollback. Each phase must be independently reversible. If phase two goes wrong, rolling back to phase one must work, which it does precisely because both columns still exist and both are still written.\n\nConstraints deserve their own care. Adding a `NOT NULL` constraint to a column old code does not populate breaks the old code's inserts. Add the column nullable, backfill, get all writers writing it, and only then add the constraint — a fourth step in practice.\n\n## Knowing when to contract\n\nContracting is the step people rush, and the one where the damage is unrecoverable — you dropped the column.\n\nVerify rather than assume. Reading the code is not sufficient, because raw SQL, reporting tools, BI connections, and scripts nobody remembers all touch the database. Better signals:\n\n**Monitor actual usage.** Many databases can report on column access or let you log queries touching it. Zero references over a meaningful period is evidence.\n\n**Wait longer than feels necessary.** A weekly report referencing the old column will not show up in a day of monitoring. Let a full business cycle pass.\n\n**Stop writing before dropping.** An intermediate deploy that stops writing the old column, left running for a period, surfaces anything still depending on it while the column still exists and can be restored to service.\n\n## Common mistakes\n\n- **One deploy for everything.** Reintroduces exactly the breakage the pattern prevents.\n- **Adding NOT NULL too early.** Breaks writes from code that does not know the column.\n- **Backfilling in one transaction.** Long-running transaction, held locks, blocked cleanup.\n- **Contracting on a code read.** Raw queries and reporting tools are invisible to grep.\n- **No rollback plan per phase.** Each phase must independently reverse.\n- **Renaming rather than expand-contract.** A rename is inherently a breaking change.\n- **Ignoring lock behaviour.** Some DDL takes locks that block writes; check what your engine does for the specific operation.\n\n## FAQ\n\n**Is this needed for adding a column?**\nNo. Adding a nullable column with no default is additive and safe — old code ignores it. The pattern is for changes that alter or remove existing structure.\n\n**What about changing a column type?**\nSame three phases with a new column of the new type, dual writes, backfill, switch reads, drop. In-place type changes frequently rewrite the whole table and take a lock.\n\n**How long between phases?**\nLong enough to be confident, which is longer for the contract step than the others. Hours between expand and migrate is often fine; days to weeks before contracting is reasonable for anything with periodic consumers.\n\n**Does this apply to APIs too?**\nExactly the same reasoning at a different layer — add the new field, support both, migrate consumers, remove the old. Any interface with independently-deployed clients has this problem.\n\n## Where to go next\n\nFor the deployment mechanics this pattern assumes, read [zero downtime deployment](/blog/zero-downtime-deployment-guide-2026). For why long backfill transactions cause trouble, [MVCC explained](/blog/mvcc-explained-2026), and for testing migrations against a real database, [testcontainers](/blog/testcontainers-explained-2026).","hue":320,"metaTitle":"Expand and Contract: Zero-Downtime Schema Changes","metaDescription":"How the expand-contract pattern lets you rename and restructure database columns without downtime, and why each phase must be a separate deployment.","keywords":["expand contract migration","zero downtime schema change","database column rename","backward compatible migration","rolling deploy database","dual write pattern","schema evolution","deployment safety 2026"],"tldr":["**Old and new code run simultaneously during any deploy** — the schema must satisfy both for the entire rollout window.","**Three phases: expand, migrate, contract** — add the new shape, move the data and readers, then remove the old shape.","**Each phase is a separate deploy** — combining them reintroduces the breakage you split them up to avoid.","**Contract only after nothing references the old column** — verify with monitoring rather than by reading the code.","**Skip:** the ceremony for a purely additive change. Adding a nullable column needs no dance."],"readTime":"5 min read","wordCount":997},"related":[{"slug":"database-migration-strategies-2026","title":"Database Migration Strategies for Production (2026)","excerpt":"Schema changes fail in production for the same handful of reasons every time: locks, incompatible deploy ordering, and no rollback path. Here are the migration strategies that avoid all three in 2026.","category":"code","date":"July 31, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":320},{"slug":"how-to-write-a-database-migration-2026","title":"How to write a database migration in 2026: safe, reversible, and zero-downtime","excerpt":"Database migrations fail when they lock tables in production or cannot be rolled back. This guide covers the 2026 patterns for safe, zero-downtime schema changes in Postgres.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":305},{"slug":"how-to-write-a-migration-script-2026","title":"How to write a migration script in 2026","excerpt":"Migration scripts move data safely between schemas or systems without downtime. This guide covers patterns, tooling, and rollback strategies for 2026.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":313}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"embedding-drift-explained-2026","title":"Embedding Drift: When Your Vector Index Goes Stale","excerpt":"Retrieval quality degrades quietly as content, language, and models change. Here is what actually drifts, how to detect it, and when a reindex is unavoidable.","category":"ai","date":"August 13, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":221},"older":{"slug":"fednow-vs-ach-explained-2026","title":"FedNow vs ACH: Which Payment Rail Moves Your Money","excerpt":"Instant payments and ACH now run side by side with very different properties. Here is what each rail costs, how fast it settles, and why finality changes your fraud exposure.","category":"finance","date":"August 13, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":200}}