Pulling Out Weeds
Dead code, AI slop, and finally sorting out my garden.
I have an uncanny ability to kill any plant I come into contact with. This doesn’t bode well for the many house plants I’m trying (and likely failing) to maintain at home. However, sometimes this flaw can become a source of good.
I recently spent a weekend tearing up weeds in my garden. I’d been meaning to get round to it for a while (ever since we first moved), and by the time I did, they’d made themselves at home. They’d been there much, much longer than I had, and they put up one hell of a fight.
The thing about weeds is that they never announce themselves. Nobody plants them. They just creep in a little at a time while you’re busy with everything else. Then, one day, you look out the window and wonder how it got this bad.
Our codebases are no different. A while back, I inherited a codebase for an existing project. At some point over that project’s lifespan, the contractor who developed the application migrated it from one cloud provider to a completely different platform. The migration itself seemed to have went well. The application was running fine, pipelines were green, and everyone had moved on to the next thing.
In this new era of AI-assisted development, I have no doubts that the previous team faced pressure to get more and more features delivered quickly. Thus, nobody went back to remove the old code and infrastructure from the previous provider. What would be the point when they have 50 other features they need to get done this week? As a newcomer to the project though, I justifiably assumed that this dead code was still doing something. It took a lot of digging (and swearing) to eventually realise that it wasn’t.
Nothing to See Here
Semgrep, SonarQube and a lot of other static analysis tools don’t know or care whether code is actually running. They scan what’s in the repo, that’s their job. In this case, the static scanners kept raising vulnerabilities in the old code.
Every one of those findings has to be picked up by someone (in this case, me), traced back to code we’d stopped using, and then either fixed or suppressed. Obviously, fixing code that never runs is wasted effort. Suppressing it is arguably worse, because once a team gets used to waving findings through, it gets easier to wave through one that matters.
Thankfully, I was able to realise early on that the best way to get rid of these findings, and make the codebase more secure, was to start ripping it out. I could’ve just as easily wasted hours fixing security vulnerabilities in unreachable code “just in case”.
These static code scanners are just the most obvious example. Dependency alerts, search results and anyone new to the codebase all have the same problem. So do AI coding assistants, which will read old, unused code just as happily as the useful parts of your codebase, burning up a tonne of tokens and potentially polluting their context window in the process.
You might know the code is dead, but until it’s deleted, your tools don’t, and your readers don’t.
”For Now” Is a Lie We Tell Ourselves
I don’t think anyone left it there on purpose. The goal of the migration was to get the new platform live, and once it was, the migration felt finished. On to the next thing. No time to breathe.
There’s also a bit of nervousness about deleting code that used to run production infrastructure. What if something still depends on it? It’s easier to leave it for now, and remove it when we’re absolutely 100% certain that it’ll never ever be needed again. The trouble is that “for now” tends to last until somebody trips over it.
Too Big to Own
Dead code and tech debt have been around for as long as there have been codebases. Most projects that I’ve worked on have had a dark corner somewhere where the bodies were buried that nobody wanted to touch.
What has changed though, is how fast projects grow now. AI assistants make writing code almost free, and in the rush to be more and more productive, it’s easy to accept more than we can really keep track of. Each change may be small and look fine on its own, but it adds up. Before long, a lot of the codebase is code nobody has read closely enough to know whether it still matters. (I wrote more about this in How I avoid becoming a stranger in my own codebase.)
When code grows faster than we can look after it, we slowly stop owning it.
Finish the Job
The clean-up itself thankfully wasn’t difficult. However, in an ideal world, this pruning should be treated as part of the migration from day one, rather than a tidy-up for later.
The rate of delivery can feel relentless these days. However, these are the kinds of tasks that need dedicated time set aside, not squeezed in whenever there’s a spare moment.
If we just keep churning out more and more code, without refining what we actually need, it will come back to bite us eventually.
In this case, deletion belongs in the migration plan itself, not left as a tidy-up for later. Carrying out any removal in its own pull request (PR) helps too. Ideally, this deletion should be kept well away from feature work, since a PR that only deletes things is much easier to review, test and revert if needed.
In my mind, nothing really needs to be kept around “just in case,” either. Git remembers everything, so the code isn’t lost, just out of the way. You can always get it back if you’re using source control properly.
Now I Am Become Death, the Destroyer of Nettles
After a weekend of ripping weeds out of my garden with an underpowered hedge-trimmer, I had to confront what I’d always known deep-down to be true. The longer I’d put this off, the harder it became when I eventually did get around to tackling it.
AI is going to keep making our codebases grow faster. And that’s fine, as long as someone is still doing the weeding. Regularly.