At some point, every successful application runs into the same uncomfortable truth: the database schema you started with is not the one you need now.
Maybe you’re adding a new feature, untangling a monolith into services, or finally replacing a “temporary” json blob that’s been in production for five years. Whatever the trigger, you now have to change a live database that real users are hitting — ideally without corrupting data, blowing your error budget, or spending the night watching a progress bar.
That’s where database migration strategies come in. They’re repeatable patterns for evolving schemas and data safely, trading off downtime, risk, and complexity in different ways.
In this article we’ll look at:
- Big-bang (“stop-the-world”) migrations
- Expand/contract (parallel change)
- Copy-and-cutover with new tables or databases
- Online schema change tools
- Dual-write / dual-read strategies
- View-based compatibility layers
- Blue/green database environments
- Event-sourcing and replay-based migrations
- Evolutionary database design and micro-refactorings
We’ll also talk a bit about where these ideas came from and how to choose a path that matches your system.
The core constraints: downtime, safety, complexity
Every migration strategy tunes three variables:
- Downtime – How much “maintenance window” can you get away with? Seconds? Minutes? None?
- Safety – If something goes wrong, how easily can you get back to a known-good state without losing data?
- Complexity – How much engineering effort, tooling, and process are you willing to invest to reduce risk and downtime?
As your system grows, you generally trade more complexity for less downtime and more safety. At small scale, it’s often the other way around.
1. Big-bang (stop-the-world) migrations
Before CI/CD, feature flags, and “zero downtime” became common vocabulary, this was the default way to change a database: you schedule a maintenance window, take everything offline, and run a big script. Many teams still do this for smaller systems, and a lot of “DBA folklore” comes from the era when this was the only realistic option.
In legacy environments (mainframes, early client–server apps), big-bang migrations were often tied to quarterly releases or weekend cutovers. The entire stack—app servers, database, sometimes even the OS—changed at once. The model is simple, but it doesn’t age well as systems get larger and more global.
- How it works
- Announce a maintenance window.
- Put the application into maintenance mode or shut it down.
- Run schema and data migration scripts directly on production.
- Verify basic checks, then bring the app back up against the new schema.
- When to use it
- Early-stage products with small datasets.
- Internal tools where a short outage is acceptable.
- One-time structural changes that don’t justify building complex tooling.
- Systems with well-understood peak/off-peak windows.
- Pros
- Straightforward mental model: old → new in one pass.
- No need for backward-compatible code paths.
- Simple release story: one app version, one database state.
- Cons
- Requires downtime, sometimes more than you expect.
- If the migration overruns, your outage grows with it.
- Rollback can mean restoring from backups (slow and stressful).
- Not workable for most 24/7 or globally distributed systems.
2. Expand/contract (parallel change)
Expand/contract (also called parallel change) is the modern workhorse for “zero-downtime” schema changes. The idea became popularized in agile database design circles and in books on evolutionary architecture: instead of doing a breaking change in one step, you introduce a new version alongside the old, migrate gradually, and only then remove the old.
Historically, this pattern gained traction as teams moved from big quarterly releases to continuous delivery. Once you’re deploying multiple times a day, you can’t afford migrations that require the code and schema to change in lockstep. Expand/contract gives you breathing room between “database shape” and “deployed code.”
- How it works
- Expand: Change the schema so it can support both old and new representations (e.g., add new columns or tables, keep the old ones).
- Migrate: Update code to read/write both versions where needed, and backfill or transform existing data.
- Contract: Once all code and data use the new structure, remove the old columns/tables in a later deployment.
- When to use it
- Any time you need a breaking change in a production relational database.
- Systems with CI/CD where many small deployments are normal.
- Changes like renames, splitting/merging tables, or altering constraints.
- Environments where you can’t easily take downtime.
- Pros
- Very low risk: each step is small and reversible.
- Supports gradual rollouts and rollbacks of application code.
- Fits nicely with feature flags and canary deployments.
- Works well across multiple services that share a database.
- Cons
- More steps to plan and implement; feels “slower” than a big-bang.
- Database can look messy during transition (extra columns, duplicated data).
- Requires discipline to actually complete the “contract” phase and clean up.
3. Copy-and-cutover (new tables or new databases)
Copy-and-cutover is like expand/contract, but with a stronger separation: instead of reshaping a table in place, you create a new structure (often a new table or even a new database), copy data to it, and then switch over. If you’ve ever seen a *_v2 table in a schema, or a “shadow” database being populated, you’ve likely seen this pattern in the wild.
This style has roots in large relational migrations and mainframe/ERP cutovers, where teams would run a new system in parallel, backfill from the old one, and then perform a “go-live” weekend where traffic is redirected.
- How it works
- Create a new table or database with the desired schema (e.g.,
users_v2). - Backfill data from the old structure to the new one, often in batches.
- Optionally keep the new structure in sync (via triggers, CDC, or job) until cutover.
- Switch application reads/writes to the new structure.
- Retire or archive the old table/database once you’re confident.
- Create a new table or database with the desired schema (e.g.,
- When to use it
- Major redesigns of a table or schema, not just small tweaks.
- Moving between database clusters or storage engines.
- Cleaning up long-lived “legacy” tables that are hard to change in place.
- Situations where you want multiple rehearsal runs of the migration.
- Pros
- Clean separation between old and new worlds.
- Easy to run comparisons between old and new results.
- Allows for “dress rehearsal” cutovers before the real one.
- Works well in combination with blue/green or dual-write strategies.
- Cons
- Requires extra capacity for duplicated data and traffic.
- Backfill and sync logic can be complex.
- During transition, you effectively have two sources of truth to manage.
- The cutover point is still a moment of elevated risk.
4. Online schema change tools
As datasets grew into hundreds of gigabytes and terabytes, teams discovered that “just run ALTER TABLE” is a great way to lock a hot table for a very long time. To avoid that, specialized tools emerged—first as scripts and DBA utilities, and later as open-source projects—designed to perform online schema changes without blocking production traffic for more than a brief moment.
These tools (like pt-online-schema-change, gh-ost, pg_repack, etc.) encode a pattern that DBAs had been hand-rolling for years: copy the table, keep it in sync, then swap it.
- How it works
- Create a “shadow” table with the desired new schema.
- Copy existing rows from the original table in chunks.
- Keep the shadow table in sync with ongoing writes (using triggers, log tailing, or replication).
- Perform a short, mostly-atomic swap—usually table renames—so the new table takes over while minimizing locks.
- When to use it
- Very large or heavily-used tables where direct DDL would cause long locks.
- Production databases where you can’t afford more than a brief blip.
- Migrations that involve adding indexes or columns to hot tables.
- Environments where you already have mature operational tooling.
- Pros
- Minimizes downtime for large schema changes.
- Widely battle-tested in high-traffic setups.
- Often integrate with existing migration frameworks and scripts.
- Pairs nicely with expand/contract for “logical” changes.
- Cons
- Adds operational complexity and learning curve.
- Tools are engine-specific and have their own quirks.
- Not a replacement for planning; application-level compatibility is still your job.
- Can be tricky to debug when something goes wrong mid-operation.
5. Dual-write / dual-read strategies
As architecture trends moved toward microservices and polyglot persistence, a new challenge appeared: migrations that cross storage boundaries. Think: moving user data from a monolith database into a dedicated “accounts” service, or from SQL to a document store.
In these scenarios, you often can’t just run a one-time script and call it done; you need a period where both old and new systems are moving in parallel. That’s where dual-write/dual-read strategies come in.
- How it works
- During migration, the application:
- Dual writes: sends each write to both the old and new data stores.
- Dual (or shadow) reads: reads from one store and optionally verifies against the other, or chooses which store is authoritative based on a flag.
- A background process backfills the new store with historical data.
- Once the new store is complete and tested, you:
- Switch reads fully to the new store.
- Eventually stop writing to the old store and decommission it.
- During migration, the application:
- When to use it
- Moving data across services or storage technologies.
- Splitting a monolithic database into per-service databases.
- Migrating between cloud providers or regions.
- Any scenario where you want to run the new system under real load before committing.
- Pros
- Enables gradual, low-risk transitions between systems.
- Lets you compare behavior under real production traffic.
- Works well with canary and feature-flag rollouts.
- Can help surface subtle differences in validation, indexing, or query semantics.
- Cons
- Complex failure modes (what if one write fails and the other succeeds?).
- Requires reconciliation and monitoring plans for data divergence.
- Increases write-path latency and operational overhead during migration.
- Easy to forget to remove the dual-write logic once the migration is done.
6. View-based compatibility layers
SQL views have been around since the early days of relational databases, originally as a way to provide virtual tables and abstract away underlying complexity. Over time, teams realized they can also be used as a kind of compatibility shim: change the physical schema under the hood, and use views to keep the logical interface stable while applications catch up.
This pattern is especially common in large organizations where many different apps and reporting tools hit the same database.
- How it works
- Change your underlying tables to match the new design (e.g., normalize, split, or rename them).
- Create SQL views that:
- Use the old table and column names.
- Project or join the new tables into a shape that matches what legacy consumers expect.
- Gradually update applications to query the new structures directly.
- Retire legacy views once nothing depends on them.
- When to use it
- Databases shared by many different applications and teams.
- Reporting and analytics environments with lots of ad-hoc queries.
- Migrations where changing all consumers at once would be impossible.
- Cases where you want to keep a stable “contract” while refactoring underneath.
- Pros
- Decouples physical schema changes from application changes.
- Gives external consumers more time to adapt.
- Can simplify permissions by exposing only views, not base tables.
- Works with any tool that understands standard SQL.
- Cons
- Complex views can hurt query performance and confuse query planners.
- Over time you can accumulate messy layers of legacy views.
- Doesn’t help if the change breaks deep application assumptions, not just shape.
- Debugging performance issues through multiple view layers can be painful.
7. Blue/green database environments
The blue/green deployment idea came from the need to reduce downtime when rolling out new versions of an application: you keep “blue” (current) and “green” (next) environments, and switch traffic between them. Over time, teams extended that concept to include databases as well, especially for big version upgrades or platform moves.
Instead of carefully upgrading a live database in place, you stand up a parallel environment, sync it, test it, and then flip the switch.
- How it works
- Maintain two environments:
- Blue: current production app + database.
- Green: new version of app + database (new schema, new engine, new cluster, etc.).
- Replicate data from blue to green, either continuously or via a carefully timed snapshot.
- Test the green environment thoroughly (staging data, shadow traffic, etc.).
- Cut over by switching routing (load balancer, DNS, service mesh) from blue to green.
- Optionally keep blue live for a while as an emergency fallback.
- Maintain two environments:
- When to use it
- Major database version upgrades (e.g., Postgres 12 → 16).
- Migrations between cloud providers or managed database services.
- High-risk, multi-step migrations where in-place changes would be too scary.
- Situations where you need a strong rollback story and can afford the cost.
- Pros
- Strong isolation: you can validate the new stack end-to-end.
- Potentially simple rollback: point traffic back to blue (with caveats).
- Minimizes risk of in-place DDL surprises on your only production database.
- Works well with infrastructure-as-code and automated environment provisioning.
- Cons
- Expensive: you’re running (and managing) two full environments.
- Handling write divergence after cutover is tricky if you need to fail back.
- Requires careful coordination of replication, DNS, routing, and secrets.
- Overkill for small or incremental schema changes.
8. Event-sourcing and replay-based migrations
Event-sourcing grew out of domain-driven design and CQRS practices. Instead of treating the database as the canonical state, you treat events (things that happened in the domain) as the source of truth. Databases become projections—derived views that can be rebuilt by replaying events.
In such systems, schema evolution looks very different: you don’t rewrite historical data-in-place; you change how you interpret it.
- How it works
- Domain events are stored in an append-only log (e.g., “UserRegistered”, “EmailChanged”).
- Read models (projections) are built by replaying events into specialized stores.
- To change the schema:
- Update the projection code to build a new schema.
- Spin up a new projection store (e.g., a new read model database).
- Replay all events to build the new model.
- Switch the application to query the new projection once it’s ready.
- If something goes wrong, you can fix the projection logic and replay again.
- When to use it
- Systems that already use event-sourcing or CQRS.
- Domains where you care deeply about the exact history of changes.
- Scenarios where you want multiple different read models over the same events.
- Complex business logic where schema evolution is frequent and subtle.
- Pros
- Very powerful: you can derive new views without changing historical events.
- Multiple projections can coexist and evolve independently.
- Helps with auditing and debugging via explicit domain history.
- Some migrations become “just” redeploying projection code and replaying.
- Cons
- Niche: not all systems are event-sourced, and retrofitting is hard.
- Replaying large event streams can be time- and resource-intensive.
- Requires careful event versioning and backward-compatibility strategies.
- Doesn’t eliminate all migration needs; projections themselves still need managing.
9. Evolutionary database design and micro-refactorings
As agile methods spread, some practitioners started applying the same principles to schemas that they already applied to code: small steps, frequent change, continuous feedback. This led to the idea of evolutionary database design and catalogs of “database refactorings.”
Instead of big redesigns every few years, you continuously reshape your schema in tiny increments. Expand/contract, copy-and-cutover, and other strategies are essentially ways to safely implement these micro-refactorings in production.
- How it works
- Represent schema changes as small, versioned migrations in source control.
- Apply changes frequently, alongside application code deployments.
- Use patterns like:
- Introduce/rename column or table.
- Split or merge tables.
- Introduce lookup tables.
- Replace free-text fields with foreign keys.
- Combine these small steps with strategies like expand/contract and online schema tools to keep production safe.
- When to use it
- Any team practicing continuous delivery or short release cycles.
- Systems where requirements and models change frequently.
- Environments where big-bang redesigns are too risky or slow.
- Teams that want to avoid “schema freeze” phases before big projects.
- Pros
- Reduces the need for massive, scary migration projects.
- Encourages good habits: tests, repeatable migrations, automation.
- Keeps schema closely aligned with the evolving domain model.
- Makes it easier to reason about each individual change.
- Cons
- Requires discipline and good tooling for migrations and rollbacks.
- Harder to communicate big-picture changes if you only ever see tiny steps.
- Some large-scale changes (e.g., new platform) still require heavier strategies.
- Legacy systems may need one or two big cleanups before this is realistic.
Choosing the right strategy
You rarely use just one of these patterns in isolation; real-world migrations often combine them. But you can still use a few simple questions to guide your choice:
- Can we tolerate downtime for this change?
- Yes, and the change is small → Big-bang might be acceptable.
- No, or only a tiny blip → Think expand/contract, online schema tools, or blue/green.
- Are we changing schema in place or moving between systems?
- In-place in a relational database → Expand/contract + online schema change tools.
- Between databases or services → Copy-and-cutover + dual-write/dual-read.
- How many consumers depend on this data?
- Many apps and teams → View-based compatibility layers and gradual deprecation.
- Just one or two services you control → Direct schema evolution is easier.
- How critical is rollback?
- “We must be able to undo this quickly” → Blue/green, copy-and-cutover, or event replay (if event-sourced).
- “We can afford some manual cleanup if things go wrong” → Simpler patterns may suffice.
Final thoughts
Database migrations are often scary not because they’re inherently dangerous, but because they’re approached as one-off stunts instead of repeatable engineering tasks.
If you:
- Treat your schema like code,
- Use patterns like expand/contract and copy-and-cutover,
- Lean on battle-tested tools for online changes, and
- Think explicitly about downtime, safety, and complexity,
then changing your database becomes just another part of evolving your system—not a quarterly “all hands on deck” event.




