The application gets rewritten every few years. The database underneath it does not. It was designed for a product that has since changed shape three times, it holds every row of data the business has ever cared about, and it is now slow, fragile, or running on a version no one supports any more. Everyone talks about modernizing the app. The risk is almost always in the database, and it is the part people are most afraid to touch — for good reason.
The database outlives the app, and holds the risk
Code is replaceable in a way data is not. If you rewrite a service and get it wrong, you fix the code and redeploy. If you migrate a database and get it wrong, you have corrupted or lost the one thing the business cannot recreate — years of transactions, customers, history that no source can regenerate. That asymmetry is why a legacy database deserves more caution than the application that sits on top of it, not less.
It is also why the database is usually the last thing to be modernized and the first thing to cause a real incident. A schema that was reasonable for the original product accretes workarounds as the business changes: columns that mean different things depending on another column, a status field with fourteen values only three of which are still used, a table everyone is afraid to alter because six systems read from it in undocumented ways. The fragility is not the database being old. It is a decade of undocumented decisions living inside it.
Left alone long enough, that database becomes the thing the whole organisation quietly organises itself around. Features that would be simple are declined because they would require touching it. The people who understood its quirks leave, and their knowledge leaves with them. What began as a technical inconvenience turns into a strategic constraint — and by then modernizing it is no longer optional, it is overdue.
Why a big-bang migration is the wrong instinct
The tempting plan is to build the new database, write a script that moves everything across, run it over a weekend, and point the application at the new one on Monday. It reads as clean and decisive. In practice it concentrates all of the risk into a single irreversible moment, with a rollback plan that is either untested or does not exist.
The migration script that ran perfectly against a copy meets production data it has never seen: the row with a null where the schema promised there never was one, the encoding that was technically wrong for eleven years, the duplicate that two systems each believe they own. You find these at 3am, mid-cutover, with the business offline and a clock running. A big-bang migration does not remove risk — it schedules all of it for the worst possible moment.
There is a quieter cost too. A big-bang plan demands that everything be ready at once — the new schema, every rewritten query, every dependency repointed — before anything can go live, so you get no feedback until the end, which is exactly when feedback is least useful. Incremental approaches trade the dramatic weekend for a longer, calmer path where each step teaches you something while the stakes are still low.
Assess the schema, the data, and the dependencies first
Before moving anything, you need three honest inventories. The first is the schema: what the tables actually are, which relationships the database enforces and which are only enforced by application code, where the real constraints live. The second is data quality, and this is the one that surprises people. Production data is always dirtier than anyone believes — orphaned rows, values that violate rules the current app enforces but old data predates, encodings and formats that drifted over years. You cannot migrate data you have not measured.
The third inventory is dependencies, and it is the one that stalls migrations. Old databases rarely have a single owner. A reporting tool reads directly from a table. A nightly batch job writes to another. A partner integration expects a specific view to exist. Some of these are undocumented and you will only find them when they break. Mapping who touches the database — and how — is not preparation for the work; it is a large part of the work itself.
These inventories are also where the schedule becomes honest. A migration estimated before anyone measured the data quality is a guess, and usually an optimistic one, because the surprises all push in the same direction. Doing the assessment first does not slow the project down — it moves the discovery of bad news from the middle of a cutover to the start of a plan, where it is cheap to absorb.
Move in slices: replicate, dual-run, migrate
The safe shape borrows from how careful teams replace anything load-bearing: you never flip from old to new, you run them side by side until the new one has earned trust. In practice that means replicating data from the old database into the new one continuously, so the new schema is always populated and current. Then you dual-run — the application reads from the old database but also reads from the new one and compares, or writes to both — so you can see divergence in production without depending on the new path yet.
Only once a slice of data and the queries against it have run correctly in parallel long enough to trust do you move reads, and then writes, over to the new database for that slice. You migrate a table, a bounded domain, a well-understood corner at a time, and each move is small enough that its rollback is a real, tested thing rather than a hope. The old database shrinks in responsibility until nothing needs it, and only then does it go away.
The discipline that makes this work is refusing to skip the parallel period even when the new path looks obviously correct. The whole value of dual-running is catching the case you did not think to test, and that case is by definition the one you are confident does not exist. Give it time to appear on its own terms, in production, while the old path is still there to fall back to.
Integrity and downtime are the real constraints
Every technical decision in a database migration answers to two masters: data must stay correct, and the business must stay up. These are the constraints that actually shape the plan — not which database engine is fashionable. Correctness is why you dual-run and reconcile continuously rather than trusting a single migration pass; a mismatch found by an automated comparison in a dual-run is a bug report, while a mismatch found by a customer after cutover is an incident.
Downtime is why the slicing matters. Most businesses can tolerate a genuinely tiny, planned, well-communicated switch for one bounded slice. Almost none can tolerate an open-ended outage because a monolithic migration hit data it did not expect. Design so that the worst case for any single step is small and reversible, and you have converted the frightening question — what if the migration fails — into a manageable one.
It helps to state both constraints out loud before any technical choice, because they resolve arguments that otherwise run in circles. When two approaches are debated on elegance, ask which one keeps the data provably correct and which one keeps the business online, and the elegant-but-riskier option usually loses on its own merits. The constraints are not obstacles to the design; they are the design.
Choose the target for fit, not fashion
It is easy to pick the new database by reputation, or because a competitor uses it, or because it was in a conference talk. Choose it instead by the shape of your data and the questions you ask of it. A system built around transactions and strict consistency has different needs from one built around flexible documents or one built around heavy analytical queries. Matching the engine to the workload matters far more than picking the currently admired name; the wrong fit will make you fight the database for years.
Be equally honest about operational reality. A database your team can actually run, back up, monitor, and reason about beats a theoretically superior one that no one on staff has ever operated at 3am. Modernization is not only about the engine — it is about landing on something your people can own for the next decade.
Modernization is a data project, not a rewrite
It is worth being clear about what this is and is not. Replacing the application around a database, or restructuring an old codebase, is a different discipline — that work is about behaviour and can be sequenced by feature. Modernizing the database is about the data itself: its integrity, its history, its dependencies, its correctness through the move. The two often happen near each other and get conflated, but the database work has its own risks and its own careful pace.
Treating the data layer as a subtask of an app rewrite is how the app ships and the data quietly breaks — the deadline belongs to the visible thing, so the migration gets rushed to fit it, and the corners cut are exactly the ones that cannot be safely cut. Give the data layer its own plan, its own assessment, and its own respect. It is the part you cannot recreate, and it will outlive whatever you build on top of it, so it is worth getting right on its own terms.