There is a moment in the life of most business applications where changing anything becomes frightening. The system still works. It still runs the business. But a one-line change takes three weeks, two people understand it, and nobody wants to deploy on a Friday.
The instinct at that point is to rewrite. It is almost always the wrong instinct, and the reason is not sentimentality about old code.
Why rewrites disappoint
A rewrite proposes to reproduce, in eighteen months, behaviour that accumulated over eight years. Most of that behaviour is undocumented, and a meaningful portion of it is load-bearing — the special case for one large customer, the rounding rule that matches an external system, the overnight job that quietly fixes bad data from a decade-old import.
You will rediscover these one at a time, in production, after go-live. That is the actual cost, and it does not appear in the estimate.
Meanwhile the business does not stop. Feature work either freezes — which makes the programme politically fragile — or continues on the old system, which means you are now maintaining two systems and the finish line moves as you approach it.
The rewrites that succeed usually share one property: the old system was small, or genuinely dead. Neither describes the system you are worried about.
What actually makes change expensive
Before restructuring anything, find out where the difficulty actually is. In our experience it is rarely where the team assumes, and almost never where the executives assume.
Common culprits, roughly in order of how often they turn out to be the binding constraint:
- Deployment. If releasing takes a day and involves manual steps, changes get batched, batches get risky, releases get rarer, and each one gets bigger. This is a doom loop and it is usually the first thing to fix.
- Absent tests around change-prone code. Every change is verified by hand and hope, so every change is slow and frightening.
- Unsupported framework and dependency versions. These block everything else and accumulate security exposure quietly.
- Unclear boundaries. Business logic in event handlers, data access in views, one class that does eleven things.
- Environment drift. “It works on staging” is a symptom of infrastructure that is not reproducible.
Note that only item four is what people usually mean by “the architecture is bad”.
The sequence
1. Make it deployable
Get to a repeatable build and an automated deployment before touching structure. It is unglamorous and it is the highest-leverage work available, because every subsequent step is safer and faster once you can release confidently.
If you do nothing else on this list, do this one.
2. Put tests around what you are about to change
Not comprehensive coverage — characterisation tests around the specific modules you intend to touch. The purpose is not correctness in the abstract. It is a safety net under the next change.
// A characterisation test records what the system DOES, not what
// it should do. If the behaviour is wrong, that is a separate
// decision — first make it impossible to change accidentally.
[Fact]
public void Invoice_total_matches_legacy_rounding_for_split_shipments()
{
var invoice = LegacyInvoiceBuilder.FromFixture("split-shipment-2019-11.json");
var total = _calculator.Total(invoice);
Assert.Equal(1284.37m, total); // verified against production output
}
Yes, this pins in behaviour that may be wrong. That is deliberate. You cannot safely fix behaviour you cannot reproduce.
3. Remediate versions and dependencies
Unsupported .NET Framework versions, packages with published vulnerabilities, and libraries that block a runtime upgrade. Dull, bounded, and it unblocks everything after it.
4. Establish boundaries inside the existing system
Now structural work becomes safe. Introduce modules with clear ownership of their own data. Move business logic out of UI and infrastructure layers. Enforce the boundaries with tooling rather than good intentions — an architecture test in CI is worth more than a document nobody reads.
At this point you have a modular monolith, which for most business systems is the correct destination rather than a waypoint.
5. Strangle what genuinely needs replacing
Only now, and only for components with a real reason. The strangler-fig pattern: route traffic through a facade, implement the new version behind it, move callers across gradually, delete the old path when nothing points at it.
Two properties make this defensible where a rewrite is not. The system is in production the whole time, so you learn continuously rather than at the end. And you can stop after any component and still be better off than when you started.
That second property is what makes incremental modernization fundable. Every phase delivers standalone value, so the programme survives a change of budget, priorities or sponsor. A rewrite has value only at the end, which is exactly when it is most likely to be cancelled.
What to leave alone
Not everything old is a problem.
A module that is stable, rarely changes, and does its job is not technical debt. It is an asset with an unfashionable implementation. Rewriting it converts working software into a risk, and buys nothing except consistency — which is not a business outcome.
The build/refactor/replace/retain decision should be made per component, and “retain” should appear more often than teams expect. Modernization budget spent on code nobody touches is budget not spent on the code that is actually hurting.
How to sequence the roadmap
Order by risk reduction per unit of effort, not by how much the code offends anyone.
That usually produces a first phase that reads as disappointingly mundane: automate the deployment, add tests to the billing module, upgrade seventeen packages. No new architecture diagram. But by the end of it, the team can change the system without fear — and every subsequent phase becomes cheaper.
The architecture diagram comes later, and it will be better for having been drawn by people who by then understand where the real constraints were.
Measuring it
Baseline before starting, so improvement is demonstrable rather than felt:
- Lead time from commit to production
- Deployment frequency
- Change failure rate
- Time to restore after an incident
- Defect escape rate
- Estimate variance
These are also the numbers that let you argue for the next phase. “Delivery feels better” persuades nobody at budget time. “Median lead time went from eleven days to two, and change failure rate halved” is an argument.
The short version
Do not rewrite. Make it deployable, make it testable, make it supported, then make it modular — and only then replace the parts that genuinely need replacing, one at a time, with the system running throughout.
It is slower to describe and considerably faster to arrive.
Written by WeInDev. If this is a decision you are facing now, it is also a conversation we are happy to have — thirty minutes, no pitch deck.