I had the privilege of speaking on Technical Debt at DevOps Oxford last week, and although most of my focus was on talking about what "technical debt" is and how we can repay it, I wanted to include a few thoughts on whether its possible to write code in such a way that we don't accumulate large amounts of technical debt in the first place. Prevention is better than cure, right?
Practices for avoiding technical debt build-up
There are a lot of very obvious practices that will help reduce the rate at which we accumulate technical debt:
- Ensuring we write "clean code" and follow "SOLID" coding practices
- Being disciplined about creating automated tests for all of our code
- Repaying known technical debt promptly, and not putting it on the backburner
- Not letting doing things the "quick way" instead of the "right way" become the norm. Deciding to take on technical debt should be the exception not the rule
- Using the insights gained from metrics and retrospectives to strategically drive initiatives to improve the codebase.
Technical debt is proportional to lines of code
All of these ideas ought to decrease the rate at which technical debt accumulates in our projects. But irrespective of this, I've come to think that technical debt is proportional to the number of lines of code. The more code you have, the more technical debt you have.
That doesn't mean that two projects each containing a million lines of code will have the same amount of technical debt as the other. One may be on the verge of collapse, while the other may still be very maintainable. But you can be sure that in both codebases, there's more technical debt now than there was when there were only 500,000 lines of code, and more will be introduced with the next 500,000 lines to be written.
If technical debt is proportial to lines of code, then one of the most effective strategies must surely be modularization. Instead of having everything in one monolithic source code repository, look for ways to extract chunks of functionality into their own smaller projects.
This might be extracting a component (e.g. an npm or NuGet package), or moving some functionality out into a separately deployed microservice.
These modules should have their own source control, CI build, and deployment procedures. And they can be worked on independently of the rest of the system.
Well defined interfaces
Now you might be thinking - modularization can't magically make technical debt disappear. And of course it won't. But it does push it out of our way.
So long as the components or services we extract have a well-defined interface, we don't need to concern ourselves with the internal implementation details of those components, or worry about technical debt that may be hiding in there.
One question that came up at the user group is whether the extracted modules should be developed in a consistent way. Should all our components/microservices share the same tech stack, coding conventions, build and unit testing strategy etc? Obviously this sort of consistency is very beneficial, as it allows developers to get up to speed very quickly. But conversely, one advantage of breaking components up like this is that we have freedom to adopt newer and better tools, technologies and practices. So be consistent where it makes sense, but don't let it get in the way of progress.
The advantages of smaller codebases
The fact is that smaller codebases have multiple advantages compared to bigger ones.
- They fit into our tiny brains more easily. It's possible for a new developer to explore the project and fully understand it in a short period of time. This helps address the problem I call "knowledge debt" in my Pluralsight course, where the turnover of developers means there are large swathes of code that noone knows how they work.
- They are easier to refactor. In large codebases, you find that tight coupling gets introduced, and so changes as insignificant as a method signature change can have ripple on effects that take ages to resolve. With smaller codebases, "find all references" returns a managable number of results, making it much easier to reason about the impact of changing something.
- They are easier to test. When we take the trouble to extract code out into its own service or component, we introduce a "seam" that makes testing possible. By decoupling it from its consumers we make it possible to test in isolation, and by introducing a clean interface, we make it possible for its collaborators to mock.
- They are possible to rewrite or replace. Another issue I discuss in my Pluralsight course is "architectural debt" where the architecture of our system is proving an obstacle to future development, either because we just got it wrong, or because the project has moved in new and unanticipated directions. Resolving architectural debt is painful because it often requires major code upheaval. If a component or service is small enough to rewrite, then it becomes possible to introduce the newer version in an incremental fashion throughout the rest of the codebase.
- They allow migration to newer tools and technologies. This addresses what I call "technology debt" - where we get stuck on a legacy version of some technology or open source dependency, and aren't able to upgrade because the knock-on effects are too great. Modularization allows individual services or components to adopt new technologies at their own pace, without needing to change everything at once.
No silver bullet
Is modularization the "silver bullet" that means we can create huge systems that have no technical debt problems? Of course not! I'm sure you could make a horrendous mess out of lots of small components. And certainly microservices have potential to introduce a whole new class of difficulties that a non-distributed system doesn't suffer from.
But personally, every time I've found myself on a project that has a "technical debt" problem, the sheer size of the codebase has been a major obstacle to addressing the key issues. But where we've been able to break things up into more managable sizes, it becomes much easier to address the technical debt problems that are causing us the most acute pain. Not all technical debt has the same "interest rate" so we need to be strategic about what order we tackle things in.
Anyway, those are my thoughts. I know the topic of "technical debt" always generates lots of lively debate, so I'd love to hear your ideas in the comments.