The call came on a Thursday afternoon.
They'd spent £200K and 14 months with a software agency. They had a product that didn't work in ways the agency couldn't explain, a launch date they'd already moved twice, and a board meeting in three weeks where the CEO needed to present a credible path forward.
Could I look at the codebase and tell them what was wrong?
I said yes. In the next 72 hours, I found out.
The First 48 Hours
I do the same thing at the start of every rescue engagement: I read the git history before I read the code. The git log tells me more about how a team worked than the code tells me about what they built.
This project had 847 commits over 14 months. The commit messages were a diagnostic in themselves: "fix", "fix fix", "fix fix fix", "hopefully this works", "remove console.log". There were no meaningful descriptions of what had changed or why. The commit frequency told a story of three separate teams that had each worked on the codebase for three to four months before being replaced.
Three separate teams. On one project. In 14 months. That's your first red flag.
When I read the code itself, the problems were systematic. Not the kind of mistakes that come from one engineer having a bad week. The kind that come from inexperienced engineers working without mentorship, code review, or clear architectural direction.
What I Found
No tests. Zero. Not a single unit test, integration test, or end-to-end test in a codebase that had been in development for over a year and was supposed to handle financial transactions.
SQL injection. The API was constructing database queries by string concatenation from user input. On the endpoint that accepted a search term, the query looked like this:
```javascript const results = await db.query( `SELECT * FROM customers WHERE name LIKE '%${searchTerm}%'` ); ```
Any user who typed `'; DROP TABLE customers; --` as a search term would have had a very interesting effect on the database. This is a textbook vulnerability, covered in every web security course, first documented in the 1990s. It should not appear in any code reviewed by a developer with more than six months of experience.
Secrets in git history. The `.env` file had been committed to the repository in month 2 and deleted in month 3, but it was still in the git history. Database passwords, third-party API keys, a Stripe secret key—all recoverable in under 30 seconds.
A 3,800-line React component. Literally a single component that handled routing logic, data fetching, state management for seven different forms, and rendering. It had no tests, no clear separation of concerns, and changing any line in it was genuinely unpredictable in its effects.
The Architecture Decision
There are two options when you inherit something like this: rewrite or rescue.
A rewrite is appealing. Start clean, apply best practices, build the thing properly. It's also a trap. Rewrites take longer than estimated, lose institutional knowledge embedded in the existing code, and you're equally exposed to the original problems if you don't address the root cause—which is usually the process, not the code.
I chose rescue. The reason: 14 months of work, however poorly executed, contains real decisions about edge cases, business rules, and integration behaviours that aren't fully documented anywhere else. Throwing it away means discovering those decisions again, in production, under pressure.
The rescue approach: stabilise → test → refactor.
Stabilise meant fixing the immediate security vulnerabilities. SQL injection first—parameterised queries across every database call took two days. Rotating the leaked credentials. Adding authentication middleware that was missing from several internal endpoints.
Then I wrote tests. Not full coverage—that would take months. I wrote tests for the behaviour that was already broken and for the behaviour the CEO needed to demonstrate in three weeks. Just enough to have a safety net for the changes I was about to make.
Then I refactored the critical path: the transaction flow that the product depended on. I didn't touch the code I didn't need to touch. The 3,800-line component still exists. It's a known problem, on the roadmap, not a current emergency.
Three Weeks
Week one: security fixes, critical bug fixes, write tests for the demo flow. Week two: refactor the transaction processing pipeline, fix the three bugs causing transaction failures in specific edge cases. Week three: integration testing, performance testing, final fixes, demo preparation.
The demo went fine. Not perfectly—there were rough edges visible to anyone who looked closely. But the core functionality worked, consistently, under load. The CEO had something credible to show the board.
The Lessons
This happens more than it should. Here's what I tell clients before they engage any agency:
Get version control access from day one. If an agency won't give you read access to your own project's repository, that is not negotiable. The repository is how you audit what's being built.
Ask to see test coverage reports monthly. Not stories about tests being written—actual coverage numbers. A project with no tests after three months of development is a project with no safety net.
Ask to meet the engineers writing the code, not the PM managing them. One hour of conversation with the actual developer tells you more about what you're getting than ten hours of sprint reviews.
Cheap agencies are not cheap. The agency that quoted £80K was £120K cheaper than the agency that would have done it properly the first time. The rescue engagement plus the original £200K came to £280K. The "expensive" agency would have cost £160K.
The cheapest option is the one that works.