01The rewrite is the most expensive option
A large .NET backend that has been running for a decade is not badly written. It is written for requirements nobody wrote down, under deadlines nobody remembers, by people who have moved on. Every strange branch in it is a bug report from 2017 that somebody fixed at two in the morning. That is not technical debt in the accounting sense - it is undocumented business logic.
Which is why a rewrite fails so reliably. The proposal is always the same: freeze the old system, build a clean one beside it, cut over. What actually happens is that the old system keeps changing because the business keeps trading, the new one chases a moving target, and eighteen months later there are two systems to maintain instead of one. The rewrite does not fail on engineering. It fails because the requirements only exist inside the thing being replaced.
02What the situation actually looks like
The symptoms are consistent enough to be diagnostic. Deployments happen at night because nobody trusts them in daylight. There is a developer, often external, who is the only person who understands the payment module. Nobody refactors, because the last person who tried broke invoicing for two days. The test suite either does not exist or has been red for so long that a red build no longer means anything.
Underneath, the code is usually .NET Framework rather than modern .NET, tied to Windows, IIS and a SQL Server that has its own decade of accumulated stored procedures. The dependency list contains packages that stopped receiving updates years ago. None of that is fatal. All of it is fixable in an order.
03Step one: make the system observable before you make it better
The first work on a system like this is not code. It is finding out what it does. That means structured logging where there is none, error tracking that reaches somebody, and a map of which endpoints and jobs actually run - because in a ten-year-old codebase a surprising share of it does not.
Production traffic answers questions no amount of reading answers. Which of the four export formats is anyone still using? Does that nightly job still fire? Traffic data usually retires whole modules before a single line is refactored, and every module retired is one that never has to be migrated.
04Step two: cover what cannot break
Test coverage on legacy code is not a quality exercise. It is permission to change things. Without it, every refactor is a bet; with it, a refactor is a build result.
The mistake is trying to cover everything. Coverage percentage is the wrong target - the right one is the list of operations the business cannot survive being wrong. Usually that is under twenty flows: authentication, pricing, checkout, invoicing, whatever the regulator inspects. Those get characterisation tests that assert current behaviour, bugs included, because at this stage the goal is not correctness. It is knowing the moment behaviour changes.
- Characterisation tests on the critical paths first, asserting what the system does today rather than what it should do.
- Integration tests over unit tests, because legacy code rarely has seams a unit test can reach.
- A pinned, reproducible database fixture - most legacy suites fail on data, not logic.
- Every bug fixed from here on arrives with the test that would have caught it.
05Step three: automate the checks, then make them mandatory
A suite nobody runs is decoration. The tests go into CI, the build goes red on failure, and merging on red stops being possible. That single change does more for a legacy codebase than any refactor: it converts "we think this still works" into a fact that renews itself on every commit.
Alongside the tests: dependency scanning, so the packages that have quietly gone unmaintained become visible; static analysis with a warning budget that can only go down; and a deployment that runs the same way every time. The goal is that a Tuesday afternoon release stops being an event.
06Step four: draw boundaries before you move anything
Only now does modernisation proper begin, and it begins with lines rather than code. The strangler pattern works because it changes one thing at a time: put a routing layer in front of the old system, move one bounded capability behind a new service, and point the router at the new one. The old code stays until nothing calls it.
The choice of first capability decides whether the programme survives. It should be something with a clear boundary, low coupling and visible business value - notifications, reporting, file processing, search. Never start with the module everyone is afraid of. Start with the one that proves the mechanism, so the second migration is an argument nobody has to have.
07What the business gets, and when
The honest answer is that steps one to three buy no new features. They buy the ability to change the system without holding your breath, and they usually take a couple of months on a system of real size. Everything after that is faster than it was, and the gap compounds.
The commercial argument is not modernity. It is that the current state has a single point of failure who is a person, that hiring for .NET Framework gets harder every year, and that an unpatched dependency in a payment path is a business risk rather than an engineering preference. Those are the three things that make a board approve the work.
08How this went in practice
A client came to us with exactly this: a large, old .NET backend carrying serious volume, no internal resource left to maintain it, and no appetite for the rewrite two other firms had proposed. We placed an engineer with the right background inside their team rather than taking the system away from them.
The order was the one above. Learn the system, cover the paths that could not be allowed to break, put the checks in CI so nothing regresses silently, clean out what production data showed nobody used. Only then did migration to modern .NET begin, capability by capability, with the business running throughout.
It is slow work and we say so up front. It is also the version where the system stays available the whole time, which is the only version most businesses can actually afford.