Legacy modernization without downtime: when the two systems disagree
Routing traffic to a new system is the easy half. The hard half is the day the old system and the new one return different numbers, and deciding in advance which one you believe.

The short version
- The routing half of a staged migration is solved and well documented. The data half, keeping two live systems telling the same truth, is where programmes actually stall.
- Decide the system of record per capability, in writing, before you route a single request. On the first day the numbers differ, finance, support and your auditor will each ask the same question.
- Set the parity threshold and the observation window before you see the first comparison. A threshold chosen afterwards is a negotiation with yourself, and it resolves in favour of the date.
- Rollback is a decision, not a capability. Name the trigger, the metric and the one person who can call it, and know the moment past which reversing is no longer possible.
- A facade only intercepts requests. Batch jobs, month-end close, file drops, partner integrations and webhooks registered against old URLs go around it, and each needs its own migration.
Everyone stops at the same place
The advice on replacing a system that cannot go offline is remarkably consistent. Do not attempt a single switchover weekend. Put a facade in front of the old system, route one capability at a time to the new one, and let the old system shrink until there is nothing left of it.
That advice is correct, and it is the easy half. It describes how requests find their way to the right code. It does not describe what happens for the months in the middle, when both systems are live, both are being written to, and one Tuesday morning the finance lead notices that the two of them do not agree.
In July 2026 we read eleven pages that search engines returned for queries about modernizing legacy systems without downtime, including the reference architecture pages from two major cloud providers. Seven describe the routing pattern. One gives any mechanics for rolling back. None of the eleven states what to do when the old and new systems return different answers. Not one names a resolution rule, a tolerance, or a person who decides.
That gap is not academic. It is the thing that turns a six month migration into a two year one, because a team that cannot agree on which system is right cannot cut over, and a parallel run that nobody is allowed to end becomes permanent. This article is about the half nobody writes down.
What the strangler pattern actually gets you
The pattern has a name and a canonical description. Martin Fowler named it in 2004 after strangler figs he had seen in Queensland three years earlier, plants that seed in the branches of a host tree and work their way down until they root in the soil and the original tree is gone. His current write-up supersedes that original. Both major cloud providers publish reference architectures for it: Microsoft as the strangler fig pattern and Amazon in its prescriptive guidance.
The mechanism is simple. A facade sits in front of the legacy system and intercepts incoming requests. At first it forwards everything to the old code. As each capability is rebuilt, the facade starts routing that capability to the new implementation instead. The callers never change. The old system loses responsibilities one at a time until it holds nothing and can be switched off.
What it does not address is the state underneath. Two implementations of the same capability read and write data. For as long as both are live, that data has to be in two places, or in one place understood two ways, and something has to keep them honest. Amazon is candid about this in its own guidance, describing the redundancy and eventual consistency between two data stores as something to treat as a tactical position rather than a destination. It is the right caveat. The rest of this article is what to do about it.
One thing worth settling before any of it: the goal is not to reproduce what the old system does. Thoughtworks put rebuilding to feature parity on hold in its own technology radar, and the reasoning is worth quoting because it is easy to forget mid-programme. Current business processes are in large part a consequence of the workarounds and compromises the legacy technology required. Rebuilding those faithfully is waste, and it spends the one opportunity you get to build what the business actually needs now.
Two systems, one truth
Dual writes fail in the middle
The obvious approach is to write to both systems on every change. Stripe published the canonical version of this sequence: dual write to old and new, move reads across once you trust them, then move writes, then remove the old data. It is a sound plan and it is the one to follow.
It is also worth understanding exactly how the naive version breaks, which Martin Kleppmann set out in why dual writes are a bad idea. Two failure modes matter. The first is partial failure: the second write fails after the first has committed, there is no transaction spanning both, and nothing in the code knows the two have diverged. The second is subtler and worse. Two clients write concurrently, the two systems receive their requests in different orders, and both end up internally consistent, permanently different from each other, and completely silent about it.
The fix in both cases is to stop treating the second write as part of the request. Commit once, and in the same transaction record that a change happened, then let a separate process apply it to the other system and retry until it succeeds. The write and the intent to replicate it commit together or not at all, which is the only guarantee available without a distributed transaction. Change data capture, reading the database log rather than the application, achieves the same ordering guarantee from the other direction and is usually the better option when you cannot modify the legacy code at all.
Backfill is a project, not a step
Replication handles changes from the moment you turn it on. Everything that existed before then has to be copied across separately, while the system stays live. That work is routinely written into a plan as one line and it deserves several. It has to run in chunks small enough not to starve the production database, it has to be throttled against real load, it has to be restartable from where it stopped rather than from the beginning, and it has to cope with rows changing underneath it while it copies them.
The order matters too. Backfill first and replicate second and you will miss everything that changed in between. Replicate first and backfill second, with the backfill written so that a replicated change always wins over a copied one, and the two meet cleanly.
What to do when they disagree
They will disagree. Two implementations written years apart, by different people, against different assumptions, will not produce identical output on every record, and the first time it happens should not be the first time anyone has thought about it. These are the decisions to make before you route a single request, and every one of them is easier to make in advance than in the room where the numbers are already wrong.
Decide the system of record, per capability, in writing
While both run, exactly one of them is the answer of record for each capability. Everyone from the finance lead to the support desk needs to know which, and so does whoever signs your accounts. This is not a philosophical question. It is what determines whose number goes on the invoice on the first day the two differ.
Separate the differences that matter from the ones that never will
A naive comparison lights up on timestamp precision, floating point rounding, sort order and sequence numbers that were never going to match. Publish the exclusion list and the tolerance per field before you start. Otherwise the team spends its first fortnight investigating noise and learns, exactly in time for the difference that matters, to ignore the alert.
Compare outputs, not screens
Two interfaces can look identical and be computed differently. Compare what the business actually consumes: the export, the ledger entry, the invoice total, the file the regulator reads. Run both systems against the same input and diff the artefact.
Set the parity threshold and the observation window before you see a single result
Decide what proportion of records must match, across which entities, sustained for how long. A threshold set after the results are in is a negotiation with yourself, and it always resolves in favour of the date that has already been announced.
Quarantine, do not auto-correct
When a record diverges, hold it and raise it. Never let either system silently overwrite the other. Auto-correction destroys the evidence needed to find the cause, and a migration that quietly repairs its own symptoms keeps every bug it had and loses the trail to all of them.
Give one named person the authority to stop the cutover
Not a committee, and not the delivery lead whose incentive is the date. Someone who can say that parity is not good enough this week and be listened to. A rollback plan nobody is empowered to trigger is a document, not a control.
Rank the entities by what a wrong answer costs
A rounding difference on an internal dashboard is noise. The same difference on a tax filing is not. Rank by consequence, then let that ranking drive both how hard you compare each entity and the order in which you migrate them. Start with the capability where being wrong matters least, because that is where you learn the process.
Rollback is a decision, not a capability
Almost every migration plan says there is a rollback plan. Very few say what would trigger it, who calls it, or when it expires. Those three omissions are why teams sit at ninety percent parity for months: rolling back feels like failure, going forward feels unsafe, and with no pre-agreed trigger there is no way to decide.
The trigger should be written down as a metric, a threshold and a window, in the same document as the parity target. The expiry matters just as much, because reversibility has a shelf life and it is shorter than people assume.
| What you are reversing | Still reversible while | What closes the door |
|---|---|---|
| Read traffic | The legacy path still exists and is still being fed | Very little. This is the easy direction and it is why reads move first |
| Write traffic, before the new system holds anything unique | Every write is still reaching the legacy system too | The first write that lands only in the new system |
| Write traffic, after the new system holds unique data | You built and tested a reverse path that can replay new records back into legacy | No reverse path was built, which is the usual case, because nobody budgeted for a migration that runs backwards |
| A schema change | The old column or table still exists beside the new shape | The contract step ran and the old shape was dropped |
| A whole capability | The legacy code, tables and scheduled jobs are still deployed | Legacy objects were deleted to tidy up before the window had actually closed |
The row that catches people is the third. Reversing reads is trivial and everyone plans for it. Reversing writes after the new system has accepted records the old one never saw requires a reverse migration that has to be built and tested in advance, and almost nobody does. If you are not going to build it, say so explicitly, and treat the first unique write as the moment the decision becomes permanent. Fowler is honest about this limit too, noting that even with blue-green deployment there remains the problem of transactions the new environment accepted while it was live.
The practice that separates teams who cut over calmly from teams who do not is rehearsal. When GitHub built gh-ost for online schema changes, the design let them run a migration against a replica, swap the tables and swap them back, and they reported completing thousands of migrations that way before running one against a production primary. The cutover itself was deliberately deferred so a human chose the moment. Rehearse the reversal, not only the change.
What a facade never reaches
A facade intercepts requests. Anything that reaches your legacy system by another route goes straight past it, and every one of these is a separate migration with its own parallel run.
| Surface | Why routing misses it | What it needs instead |
|---|---|---|
| Nightly batch and ETL | It never makes a request through your proxy | Treat each job as its own migration stage, with its own comparison and its own cutover |
| Month-end close and scheduled reports | It runs on a calendar, so a defect can hide for four weeks | Run at least one full close on both systems, in full, before cutting it over |
| Partner integrations you do not control | They post to a legacy hostname you cannot make them change | Keep the old endpoint alive as a thin forwarder and give partners a dated window to move |
| File drops over SFTP or EDI | There is no request to intercept, only a file appearing | Produce the file from both systems and diff the output before switching the producer |
| Webhooks registered against old URLs | The registration lives inside somebody else systems | Inventory every registered callback before cutover, and assume you will not find them all |
| Anything behind an IP allowlist | The new host has a different address | Raise the allowlist change weeks ahead. It is routinely the slowest item on the plan |
The last two are the ones that surface on cutover night. A webhook registered years ago by someone who has left, pointing at a hostname you are about to retire, is not discoverable from your own codebase, and an allowlist sits with a third party whose change process runs on its own timetable rather than yours.
The legacy system does not stop changing while you migrate
A migration that runs for months runs alongside a business that still needs things. The legacy system will get a new field, a changed constraint, a fix to a calculation. If your replication and your comparison assume a fixed shape on both sides, each of those changes breaks something quietly.
The discipline that makes schema change survivable has a name, parallel change, documented by Danilo Sato and more often called expand and contract. Add the new shape alongside the old one, migrate every reader and writer across, and only then remove the old. Every step is individually reversible, which is exactly the property you want during a period when reversibility is your main safety net. Fowler makes the same point about deployment order in blue green deployment: change the schema so it supports both the old and the new version of the application first, deploy that, confirm it works so you have a rollback point, and only then deploy the new application. A schema change and an application change in the same deployment is one of the more reliable ways to produce an outage you cannot reverse.
The organisational half of this is a freeze policy that people actually accept. Not a freeze on all legacy change, which no business will agree to, but a rule about which tables are in scope for the current stage and a route for urgent changes that lands them in both systems at once.
Decide in advance when both systems stop running
A parallel run costs real money and real attention. Two systems to operate, two sets of alerts, and somebody on call who has to understand both well enough to tell which one is wrong at three in the morning. That cost is acceptable for a defined window and corrosive when the window has no end.
So write the exit criteria at the start, next to the parity threshold, and make them specific: the entities that must match, the proportion, the number of consecutive days or closes, and the sign-off required. Then hold to them. The most common failure in a staged migration is not a technical one. It is a parallel run that becomes the permanent architecture because no one ever defined what finishing looked like.
The people who operate the system daily should sign off each stage, not only the engineering team. They are the ones who will notice that a report is subtly wrong, and they are the ones carrying the cost if it is.
What waiting costs, and which numbers to distrust
The case for starting is usually made with statistics, and most of the statistics in this field do not survive being checked. Two that do come from government audit bodies, which have no service to sell.
The US Government Accountability Office reviewed the federal legacy estate in July 2025 and reported that about 79 percent of planned federal IT spending for that year was going to operations and maintenance rather than new capability. Of the eleven systems it singled out as most in need of modernization, ranging from roughly twenty-three to sixty years old, eight ran on outdated languages and seven were operating with known security vulnerabilities that could not be remediated without modernizing. Of ten systems it had flagged as critical six years earlier, three had been modernized.
The UK National Audit Office put the same problem in cost terms across five large digital programmes, which between them ran about 26 percent over their original forecasts and accumulated at least twenty-nine years of delay, with the auditor attributing part of that to continuing to operate legacy systems for longer than planned. In a separate review it found that departments did not have fully funded plans to remediate roughly half of their legacy estate. Its plainest sentence is the one worth carrying: continuing to use legacy services is risky and commits organisations to even higher future costs.
When a staged migration is the wrong choice
Staged replacement is not free, and there are cases where the single cutover is genuinely the better decision. All of them assume you are replacing the system with something you build; if a product might cover the ground instead, that is a different decision with a different shape. The cases where staging is the wrong tool:
- The system is small enough to rebuild and cut over in one short window. The parallel-run machinery costs more than the risk it removes.
- You cannot intercept requests and cannot modify the legacy code. With no facade and no hook for replication, there is nowhere to stand.
- The data model is so entangled that no capability can be carved out without carrying most of the schema with it. Fix that first, or accept a full rebuild.
- Nobody on your side can own the comparison. Every rule in this article needs a person, and without them a staged migration just runs two broken systems instead of one.
Where staging is right, which for a business-critical system it usually is, the work that decides the outcome happens before any code is routed: naming the system of record, agreeing the tolerances, building the reverse path or admitting you will not, and writing down who can stop it. That is the same discipline that decides whether an ERP switchover lands, and it is the reason we plan the going-live before we plan the building.
Questions about migrating without downtime
How do you modernize a legacy system without downtime?
Put a facade in front of the legacy system, rebuild one capability at a time, and route traffic to each replacement as it is proven, so the old system shrinks rather than being switched off in one event.
The routing is the well-documented part. What decides whether it works is the data underneath: how changes are replicated between the two systems, how existing records are backfilled while the system stays live, how you compare the two, and what you do when they disagree.
What is the strangler fig pattern?
A migration pattern where a new system grows around an old one, taking over its responsibilities incrementally until the old system can be retired. Martin Fowler named it after the strangler fig, and both Microsoft and Amazon publish reference architectures for it.
Practically it means putting a facade in front of the legacy system that can route each capability to either the old or the new implementation, so callers never change while the implementation moves underneath them.
Big bang or phased migration: which is safer?
Phased is safer for any system the business depends on daily, because it lets you prove each piece with real traffic and keeps a way back. A single cutover concentrates every risk on one date, and that date tends to move.
Big bang is defensible when the system is small, the data model is simple, and a short planned outage is genuinely acceptable. Below a certain size the machinery a phased migration requires costs more than the risk it removes.
What happens when the old and new systems disagree?
You quarantine the record, you do not let either system silently overwrite the other, and you resolve it against whichever system was declared the system of record for that capability before the migration started.
That declaration is the part to get right in advance. Along with it: a published list of differences that do not count, such as timestamp precision and rounding, a parity threshold and observation window agreed before any results are seen, and one named person with the authority to stop the cutover.
How long should you run both systems in parallel?
Long enough to cover a full business cycle for the capability in question, which for anything touching finance means at least one complete month-end close, and long enough to hit a pre-agreed parity threshold on consecutive runs.
Set the exit criteria at the start rather than the end. A parallel run with no defined finish quietly becomes the permanent architecture, and you carry the cost of operating two systems indefinitely.
Can you roll back after go-live?
Reads, almost always. Writes, only until the new system holds records the legacy system never received, and after that only if you built and tested a reverse path in advance. Most teams do not.
Treat the first write that lands only in the new system as the moment the decision becomes effectively permanent, and decide before that point whether you are funding a reverse migration or accepting that forward is the only direction.
What about batch jobs and scheduled reports?
They bypass the facade entirely, because they never make a request through it, so each one needs its own migration stage with its own comparison and cutover.
Scheduled work is also where defects hide longest. A nightly job shows a problem the next day, but a month-end process can carry a fault for four weeks before anyone sees it, which is why at least one full close should run on both systems before that job moves.
How long does a legacy modernization take?
It depends far more on how entangled the data model is than on how much code there is. A system whose capabilities can be cleanly separated moves in stages of weeks; one where every table touches every other has to be untangled before any staging is possible, and that untangling is the real project.
The useful early question is not how long the whole programme will take, but how small the first genuinely shippable stage can be. If no capability can be carved out and cut over on its own, that is the finding that should reshape the plan.
Method and sources
The claims about what published guidance does and does not cover come from our own review, so here is the method. In July 2026 we read eleven pages that search engines returned for queries about modernizing legacy systems without downtime, including the strangler-fig reference pages from two major cloud providers, and recorded which techniques each named and which it omitted. Three further pages appeared in results but could not be retrieved, and nothing above describes them. Search-result ordering is not a measured ranking position and we make no claim about where any page ranks. Everything else here is judgement from our own migration work, and is written as judgement rather than as data.
- StranglerFigApplicationMartin Fowler. The canonical description of the pattern, by the person who named it
- Strangler Fig patternMicrosoft Azure Architecture Center. Reference architecture, including the phase-scoped rollback window during a database strangler
- Strangler fig patternAWS Prescriptive Guidance. Reference architecture, and the source of the caution about data redundancy and eventual consistency between two stores
- Online migrations at scaleJacqueline Xu, Stripe, 2017. The four-phase dual write, backfill, verify and cut over sequence, described by the engineers who ran it
- Using logs to build a solid data infrastructure, or why dual writes are a bad ideaMartin Kleppmann, 2015. The two failure modes of naive dual writes, and why an ordered log is the safer substrate
- Parallel ChangeDanilo Sato, martinfowler.com, 2014. Expand, migrate, contract. Frequently misattributed to Fowler himself
- Blue Green DeploymentMartin Fowler, 2010. Separating schema deployment from application deployment to preserve a rollback point, and the honest caveat about transactions accepted after the switch
- Feature ParityCartwright, Horn and Lewis, Thoughtworks, 2021. Why rebuilding to parity is the wrong goal. Thoughtworks sells modernization consulting, and put this pattern on hold in its own technology radar
- gh-ost: GitHub’s online schema migration tool for MySQLShlomi Noach, GitHub, 2016. Rehearsing migrations against replicas, and deferring the cutover so a human chooses the moment
- Information Technology: Agencies Need to Plan for Modernizing Critical Decades-Old Legacy SystemsUS Government Accountability Office, GAO-25-107795, July 2025. The federal legacy estate, maintenance share of IT spending, and modernization progress since 2019
- Government’s approach to technology suppliers: addressing the challengesUK National Audit Office, HC 543, January 2025. Cost and schedule outcomes across five large digital programmes, and the cost of running legacy systems for longer than planned
