Software developers may just be the original adherents of “the new minimalist gospel.” Overcoming that initial pang of apprehension and committing a small-but-purposeful act of decluttering, followed quickly by a flash of calm and (OK, fine) spark of joy — few know those feelings as innately as a developer who’s just chucked a line of superfluous code into the wastebin.
“For critical systems, [deleting code] can be terrifying for sure, but still feel really good,” Braintree software engineer Andrew Whitaker told Built In. “Developers typically applaud pull requests with more lines removed than added, so usually you’re doing good clean-up when that’s the case.”
It may be enjoyable, but how do development teams know when it’s really safe to toss out old code? After all, removing a chunk of code from a business-critical codebase is more consequential than, say, bagging up an old T-shirt to drop off at Goodwill. We asked Whitaker to walk us through some of the most important considerations to bear in mind before mashing “delete.”
Asking the Right Questions
First, developers have to know exactly what impact a code deletion carries. “Am I sure that deleting this code will have the intended effects?” Whitaker said. “If the code directly impacts a customer-facing feature, have I communicated with customers to let them know that the feature will change or is going away?”
It’s also smart to ask what else a developer might be able to diagnose while they’re already under the hood. Is there any additional code that can be deleted or refactored — or improved in a way that doesn’t change or add to the existing codebase’s external output? “Code is so mutable,” Whitaker said.
If a developer deletes a portion of code that facilitates communication between applications, he added, there might be a corresponding queue or an unused database that can also be decommissioned.
“A healthy development culture is going to encourage constant refactoring with any new feature.”
“A healthy development culture is going to encourage constant refactoring with any new feature,” Whitaker said. “Even a bug fix will encourage you to say, ‘Hey, let’s look at this code and see if it makes sense,’ and then work on constantly improving it — and that includes deleting unused code.”
Thorough documentation is also crucial before getting rid of old code. “You have to be ready to write as detailed a commit message as possible — what exactly did this code once do, and why can it be deleted?” Whitaker said. Diving into the granular detail can, in turn, prompt productive big-picture reevaluations too. “By explaining, you get a better understanding and possibly uncover some assumptions you need to examine.”
Questions to ask before deleting old code:
- Am I sure it will have the intended effects?
- What else can I delete or refactor?
- Am I ready to write as detailed a commit message as possible?
- What happens if this all goes wrong?
All of this prompts a related question: What’s the best method for reviewing code? That’s much-debated, and the answer varies by organization. But pair programming works particularly well when it comes to zeroing in on old and dead code, according to Whitaker.
“When you have two people who are hopefully both really deep into a piece of code, that’s much more likely to yield fruit in terms of refactoring than simply reviewing a pull request, which is unfortunately often not as deep,” Whitaker said.
Pull request tools generally only flag altered or deleted files, which means you’ll probably have to delve further into the code to see what else may be affected. “It may be hard to determine what impact that change has on the broader code base, since that context isn’t immediately present in the code review tool,” he said.
That said, a dev team that eschews pair programming but has built a truly robust system for reviewing pull requests can also find success. “If you do have a really good culture of code reviews where people are really diving deep into the change, really evaluating the entire application when viewing a pull request, that could work too,” he said.
You can also better detect unused code by automating the process to some degree. Static analysis tools will help locate unreachable code — for instance, in the form of code that appears after a function’s return statement — even if such tools aren’t optimal in every language environment.
“My experience is that those kinds of static analysis tools are better suited for compiled languages,” Whitaker said.
Cautionary tales exist when it comes to overzealous code-analysis tools, but developers are more likely to see unused code fall through the cracks in an application than risk deleting an incorrectly flagged line, according to Whitaker. “Your concern is going to be less with false positives and more with those tools not detecting things that are unused. For example, if you have a public API with a completely undocumented route, an analysis tool may not detect that,” he said. “There are different levels of checks you want and some are harder to do with those kinds of tools than others.”
Taking Advantage of Your CI/CD Testing Ground
In the wake of the DevOps revolution, it’s tempting to assume that all development teams prioritize continuous integration and continuous delivery (CI/CD) — which includes creating and testing a build whenever a code change is merged into the master branch. But it’s important to underscore how important the process is in the context of removing unreachable code and active, but redundant, code.
A well-oiled CI/CD pipeline will automate unit tests, which, over time, compound to create a robust test suite. “Those things can really instill confidence in your refactoring — your unit tests basically become refactoring insurance,” Whitaker said. Mistakes ought to be more apparent within CI.
“You should be constantly looking to improve your codebase, including removing dead code.”
“If you have those processes in place, you should be constantly looking to improve your codebase, including removing dead code,” he said.
That said, there are still situational factors to consider. If you’re a developer in an organization that still clings to the traditional quarterly release cycle, it’s probably best to refactor after each big release, rather than right before.
Added Whitaker: “Otherwise, you may have to wait until the next release to fix things or, since your releases are so infrequent, presumably they’re a big deal and you’re going to have to spend a lot of time doing another release to fix the issue.”
But doing so on the heels of a release would allow your changes to live in the test environment for as long as possible, which increases the likelihood that an issue will be detected before a feature is deployed into the wild, Whitaker said.
Still, a better solution than perfectly timing your refactoring schedule within a quarterly cycle would be to scrap that timeline altogether in favor of something more fluid and dynamic — which, of course, is easier said than done, especially for a single individual within a big development apparatus. “The root cause there is that your release process needs to be easier and more frequent, but sometimes these things are hard for you to change,” Whitaker said.
A strong CI/CD structure also goes a long way in addressing what might otherwise be a nasty pain point: wrestling with whether to delete code that was written by a developer who no longer works with the organization.
“If you have a strong development culture and continuous integration, no matter who’s there, you should feel confident that, if you remove something and nothing broke, things are probably OK. If you don’t have that in place, it’s not easy to test in that way,” he said. “You really just have to do as much testing as possible in a QA environment or a development environment to see if your change worked.”
And as long as you’re using some source control management, you can always reference old commits and builds and see the history of whatever code is on the chopping block.
“From there, you’ll often see references to other work-tracking tools and maybe can find others who know about the feature,” he said. “At a large organization, it’s rare that only one person has touched a piece of code.”
And don’t fret that a version-control backup will foster some sort of delete-happy code slaughterhouse; the fear of deleting superfluous code is much more insidious. “If you have pair programming or code reviews where everyone agrees that a line of code can be deleted, then why not? That’s a good thing,” he said. “If it’s unused and it doesn’t have to be there, then yeah, let’s get rid of it. Worst case, you can dig it back up.”
Look for Opportunities from Day One
So, at what point in an organization’s life cycle should clearing out old code become a concern? Immediately.
“Ideally, you’d be constantly looking for ways to improve your code, which includes refactoring and removing old code — that’s from Day One on,” said Whitaker, adding that even days-old projects provide ample opportunity for deletions, revisions and clean-up.
Of course, business realities will often prevent devs from trimming as much fat as they might like — the best is the enemy of the good, as Voltaire opined in a famous moment of project-management prescience. But even though all software development projects require taking on some amount of technical debt, it’s important to stay vigilant against accumulating too much.
“When you start, a little bit of technical debt isn’t a big deal,” Whitaker said. “But as you write more and more code, and the debt piles up, you end up in a situation where it can be hard, or even impossible, to add features because there’s so much crust in your code, and it’s so poorly written.”
That’s right: impossible.
Whitaker recalled a situation while working for a previous employer where the thicket of unruly code had grown so tall that the team was rendered temporarily incapable of releasing new features. “It took probably a month or two of work to get to a place where we were able to add new things,” he said.
“If left unaddressed, poor code can really bite you.”