Good Code Can Be Easily Deleted: Tips on Dealing with Technical Debt
This week I got to sit down with Robby Russell on the Maintainable Podcast to chat about technical debt: what it is, how to avoid it, and how to get rid of it when you’re already knee deep in it. I feel that there were a few ideas I didn’t say very elegantly (doing a live podcast is hard!), so I thought I’d do a follow up post to clarify. If you haven’t had a chance to listen to the episode yet, I recommend you do that first before continuing to read.
What is technical debt?
First, let’s define our terms. When it comes to “tech debt”, it seems there is disagreement in the community as to what that actually entails. Is really old code considered “tech debt” if it’s fully functional, bug free and has excellent test coverage? What if the original author is no longer around? What if it’s got terrible documentation?
On the podcast, I define “tech debt” with a story. Imagine it’s spring time. The snow has just melted and you decide it’s finally time to repaint your house. You buy the tools you need, the paint, a bucket, and set up the ladder so you can get to work. The project takes a few days, and you’re almost done when you realize your gutters need cleaning first. You leave the painting stuff on the driveway for a moment and go looking for your gloves. You get to work on the gutters.
By now it’s summer. The house isn’t fully painted yet, but at least the gutters are mostly clean. Your kids start taking out their skateboards, bikes and scooters out of the garage. Your driveway is still covered in painting supplies and now that the kids are out every day, you can’t possibly find the time to finish the task. Your neighbor stops by unexpectedly to drop off that garden hose he borrowed. You weren’t home, so he added it to the pile that’s now building up in front of your house.
In the fall, the pressure mounts. You realize if you don’t finish the paint job, you’ll be in deep trouble come winter. You dig out the painting supplies but the paint is now dry so you run to the store to get something new. Meanwhile, the leaves and acorns from the oak tree in your yard have already started falling and they’ve covered up a lot of the stuff you left out. Some of them stuck to the wet paint. You decide that in order to do the job right you should probably clear the leaves first. You go to buy a leaf blower.
And that’s when disaster strikes. The snow comes early this year and it’s a massive storm. Everything is now covered by 3 feet of snow. You get out your shovel but every time you dig, your shovel bumps up against something new: a bicycle, a paint bucket, a garden hose. It’s impossible to get anything done. At this point, you don’t even remember what sort of stuff is buried under all that snow and every time you dig, you find a new problem that needs solving. Oh, and one more thing — a snow plow drives by every evening and dumps more white powder into your driveway undoing all the work you did the previous morning.
To me, “tech debt” is all the stuff from your past that gets in your way — preventing you from having the confidence to do your job quickly and efficiently, even if that stuff was useful at some previous time.
How to avoid it
I’m sure you would agree, the best scenario is to ensure you don’t get into such a state to begin with. This is why a lot of my energy goes into “tech debt” prevention.
Clean your room
Clearing the snow would have been an easy task if you didn’t have all that mess under there. Even though every single item you pull out of the snow was very useful at some previous time — right now it’s preventing you from getting your immediate task done. It’s making you pull your hair out. For this reason, I generally ask my team to identify, document, and clean up any tech debt that was left behind after a project. This doesn’t always mean immediately fixing it so that no debt is left behind (that could take a long time). It just means tucking all the messy stuff into an orderly, organized place. For example:
- Say you had to implement an abstraction that you aren’t totally happy with. Ensure the code clearly describes in what cases it works and in what cases it doesn’t. Write unit tests to test the happy paths as well as the unhappy paths.
- Say you ended up defining a function
doSomething
that should only be used in a specific module and not exposed anywhere else. Make sure you rename that function todoSomethingInModuleX
so that it’s super clear to the next reader where it’s intended to be used. - In the rush of your implementation, your files ended up being scattered across dozens of different directories. Look for ways to collocate that code into a single directory so that it can be contained and it won’t pollute other modules. A good way to test whether your code is easy to maintain is how easy it is to delete. If you have to
grep
your whole codebase for some keywords, deleting individual lines of code at a time — it’s going to be a pain.
And if you didn’t actually manage to finish painting your whole house, see if you can schedule some time to finish the job in the next week or two. Don’t leave a “TODO for the future”. That future will never come. Shipping small MVPs is great. Set clear boundaries to define what is fully functional bug free code, and what is code that’s planning for a future problem that may never come. Don’t design architectures that look too far ahead. It’s a lot easier to expand on a working abstraction, than to modify a bad one.
Ride the daily upgrade train
Unless you work at a huge company like Google or Amazon, you’re probably relying quite heavily on community packages and projects. Whether it be an npm
dependency, or a database, or an infrastructure tool like Docker. Those community projects will improve and evolve over time. At the very least, you can expect new versions to fix known security vulnerabilities. So, how often should you upgrade?
Every single day. Assign this task to one of your engineers. Their task will be to run npm outdated
every morning and see what changed and which of those changes impact your product. Then upgrade the easy ones and document bigger breaking changes as tasks for the team to review at the next earliest opportunity. By attempting upgrading immediately you get to:
- Avoid painful multi-version migrations that could take months.
- Get immediate benefits from security fixes, performance improvements and new features.
- Discover migration pains or bugs in those packages ahead of time, rather than in the middle of a deadline.
- Keep your code modern and enjoyable to work with. Nobody likes having to support legacy code.
In my experience, the daily time investment for upgrades (albeit large) is a fraction of the time spent attempting to upgrade only when you need something. Not to mention the fact that riding on the daily upgrade train means you avoid the stress of having to do an expensive upgrade when you least expect to. Think of it as a daily “tech debt” tax.
Build in small pieces
One of the best ways to avoid “technical debt” is to ensure you architect your systems with small pieces, ie. try to build your house out of bricks rather than cement. To replace a section of a cement house requires you to sledgehammer away massive chunks, leaving big cracks you’ll have to plaster in. Whereas with bricks — you should be able to replace a single brick at a time.
For back-ends this usually means using microservices or serverless architectures. For front-ends this means embracing plugins or extensions. Ask yourself, how hard would it be for me to rewrite Feature X of my app in a whole new language without breaking anything else? If the answer is “very hard” , then your architecture could probably use an improvement. Design your modules so that they share a common API, but the internals are completely independent.
Now this doesn’t mean you should have an app that uses React, Vue and Angular all at the same time — that’s probably not a very wise decision. But what if, for example, you have a large React/Redux application and one of your teams wants to build the next feature using Apollo, just to see if it’s a better tool for that job. How easy is it for them to do this without modifying existing code? And then, afterwards, if they realize it was a terrible idea, how easy will it be for them to delete that code and start over? It should be as easy as possible.
How to dig your way out
Everything above applies to both evergreen and existing legacy projects. It’s never too late to start digging your way out (and you should start digging ASAP!). However, I have a few more tips specifically for those who find themselves working on an existing project that’s covered in a deep layer of snow.
Consider “wide” changes over “deep” changes
Let’s say you’re working with an old JavaScript project that doesn’t have any linting implement and your team wants to help improve the overall quality by adding ESLint to the project.
At first, it might be tempting to define a full set of your desired lint rules and enable those rules only for your newly written code, or a small subsection, then gradually transitioning the older bits over time. After all, incremental enhancements are a good thing! However, in my experience this actually ends up creating even more technical debt than what you started with because now you have some parts of your code following one set of best practices and another part following something else. You’ve actually made the problem worse.
A better approach is to go wide, rather than deep — replacing layers at a time. Define a small set of rules to start with that you can apply to your entire codebase, and then gradually add more rules (always ensuring you fix all violations in every file). By doing it this way, you guarantee that at any point in time you have a clean consistent state across your entire codebase. There isn’t “an old, scary, unlinted area” and then a “nice, clean, linted area”. All code is consistent all the time. Which means you don’t have any debt until you decide to tackle the next rule change.
Encourage ownership and initiative
Most software engineers don’t enjoy cleaning up debt. For most, it’s not fun and it feels like a chore, not to mention the fact that it likely requires providing justification and getting approval from your manager. However, in my experience, there is always at least one person on the team who is either so fed up with the status quo, or genuinely enjoys janitorial tasks that they’re willing to take on the initiative to move things forward on their own.
Make sure to encourage and reward this initiative. What you don’t want is for several different people on your team to be spearheading several different underground operations to clean up the code. You might end up with people pulling in different directions (developers are very opinionated after all). You want to create a culture where anyone can propose, own, and champion a “tech debt” clean up effort. It should be done in the open and as a formal project so that everyone can see the changes being proposed and others have the opportunity to volunteer to contribute to the effort.
Many of the topics I raised have been discussed at a very high level. If you’re interested in more real-world examples or specific stories of success and failure, let me know in the comments and I can add a bit more color.
Good luck with your snow removal!