A B2B SaaS, seven years old, whose only developer had left. The product makes money, but the team is afraid of releases and ships once every two months. The request was phrased as “rewrite the whole thing for us”. We proposed something else.
Why not to rewrite
A rewrite from scratch looks attractive right up to the moment it turns out that seven years of decisions nobody remembers are baked into the old code. A counterparty check that looks pointless is a reaction to an incident three years ago. An odd export format is a partner’s requirement.
The rewritten system is obliged to reproduce all of it, and the list is written down nowhere. So the first thing you do is work out how the system behaves. Code comes later.
Week 1: a map of the system
We went through the repository and the commit history and put together a picture: which modules exist, who calls whom, where the entry points are, where data crosses a boundary.
The commit history brings out what the code does not show: the files that are changed most often are either zones of active development or zones of pain. The second kind is visible from commit messages of the “fix”, “hotfix”, “fix again” variety.
The result of the week was a dependency diagram, a list of risk areas and an estimate for everything else. Before that estimate, any timeline we named would have been invention, and we said so at the start.
A separate finding: a working test environment with a copy of the production database, open to the internet with no authentication. It had nothing to do with the task, but it was the most expensive find of the week.
Week 2: a safety net
Refactoring without tests is rewriting and hoping. So the second week went on characterisation tests.
The key difference from ordinary tests: a characterisation test pins down not the correct behaviour but the current one. Even when it is wrong.
We found two such cases. In one, rounding in a report did not follow the mathematical rules, and clients had been reconciling their own figures against it for years. In the other, an API returned an error with status 200, and the partners’ integrations had been written for that.
We pinned both behaviours with tests and put them in a separate list titled “these are bugs, but they cannot be fixed without warning the partners”. The decision was the client’s, not ours.
By the end of the week the critical paths were covered at 71%—not perfect, but enough that changes stopped being a jump in the dark.
Week 3: dependencies and releases
We updated libraries that had not been touched in four years. That closed 23 known vulnerabilities. We were not looking for them; they closed themselves along with the version bumps.
The order matters here: tests first, updates second. The reverse order means that at the first breakage it is unclear whether the update broke it or merely exposed something that was already broken.
We stood up a pipeline: tests, build, deploy to the staging environment, deploy to production, roll back with one command. A full release cycle takes 12 minutes instead of two months of waiting for a suitable moment.
What came out of it
- 40 000 lines taken apart and described in a diagram
- coverage of the critical paths from 0 to 71%
- 23 vulnerabilities in dependencies closed
- a 12-minute release instead of “once every two months, when it is not scary”
Very little code was rewritten in the process. The task was not “make it beautiful” but “make changes safe”. The team will make it beautiful themselves— they now have tests that will tell them if it got worse.
What we would do differently
The environment with production data turned up on day three. Starting again, we would make it a separate half-hour step on day one: which environments exist, what data is in them and who can reach them. It is cheap and sometimes matters more than everything else.
If you are in the same situation
The first thing to do is not to look for a contractor to rewrite it. What you need is a map: what exists, what is connected, where it is dangerous. That is a week of work, and after it a conversation about timelines and budget becomes concrete.
We do that audit separately from development—including for teams that then go away and do the work themselves.