writings · 10 Apr 2026
The rewrite trap
Most teams that come to us have already attempted a rewrite. Usually more than once.
The pattern is always the same: a new team or a new leader decides the old system is beyond saving. They start building the replacement alongside the original. Six months in, the replacement is 60% done and missing the hard 40%. Business pressure forces features onto the old system while the new one stalls. The rewrite is quietly abandoned.
Then, a few years later, someone decides the old system is beyond saving.
The rewrite doesn't fail because the team isn't good enough. It fails because it solves the wrong problem. The code is a symptom. Whatever process, pressure, or practice produced that code is still in place — and a rewrite doesn't change any of that. So you build it again, faster, with more confidence, and arrive at the same place.
Understand what you have
Before anything else: does the system work? Even a buggy, fragile system that does its job is something. It represents accumulated decisions — about the domain, about edge cases, about customers — that solved real problems at some point. Working software is the true measure of progress. That includes working software you're embarrassed by.
The baseline is what you build on. Identify what's working and treat it as load-bearing until you can prove otherwise. Identify what isn't — that's your target. Identify what's covered by tests — that's your safe zone.
Understand how you got here
Why did it end up this way? Scope creep and unclear requirements? Time pressure that forced corners to be cut? A skills gap on the original team? A process that never included reviews or standards?
The answer determines what needs to change alongside the code. If the original system was built under permanent time pressure and that pressure is still there, a new codebase will accumulate the same shortcuts. The diagnosis isn't about blame — it's about not repeating the same conditions.
Make the smallest improvement, then repeat
Once you understand what you have and how you got there, the question becomes: what is the smallest change that improves the situation?
Low-hanging fruit first, if it exists. Otherwise: the biggest health and maintainability issue. Pick one, fix it, then repeat.
In practice, the first move is almost always test coverage. A system without tests can't be changed safely — which is why it doesn't get changed, which is why it stays fragile. Tests also apply design pressure: if a piece of code resists being tested, that's not a testing problem, it's a coupling problem. The code is too tangled to isolate. Dependency injection is usually the key here — the minimal structural change that makes components testable without altering behaviour.
That last constraint is critical. While you're detangling, behaviour is frozen. Structure changes. Logic doesn't. The moment you start improving both at once, you lose your reference point and you're in a rewrite by the back door.
Once you have test coverage you can start refactoring with confidence. API-level tests — asserting outputs from inputs at the boundary — are the right layer when you want to change underlying abstractions. Unit tests are what you want everywhere else.
On the question of generating tests with AI: it's genuinely feasible now in a way it wasn't before, and the economics make retroactive coverage practical for the first time. But the output needs scrutiny. Generated tests tend toward magic numbers with no explanation, and mocks that test the implementation rather than the behaviour. A test suite full of these doesn't protect you — it calcifies the system further and makes the next change harder. The goal is tests that survive the refactor.
while ( system.needs_improvement ) {
// 1. understand what you have
audit( codebase ) {
> what is working // preserve this
> what isn't // this is your target
> what is test-covered // this is your safe zone
}
// 2. understand how you got here
diagnose( history ) {
> process failure?
> unclear requirements?
> time pressure?
> skills gap?
// the answer determines what changes alongside the code
}
// 3. make the smallest improvement
next_fix =
> low_hanging_fruit // if it exists
?? biggest_health_issue // otherwise, worst first
// freeze behaviour — change structure, not logic
apply( next_fix ) {
> add tests first
> refactor under green
> verify nothing changed
}
goto 1 // repeat until done
}
The path out is the same in almost every case. Understand what the system does before touching it. Understand why it got here before changing anything. Then make one small improvement at a time, with frozen behaviour and tests to verify nothing broke. Repeat.
We help teams through this process. If you want to know where your system sits, start at legaseecode.com.