One of the things that people love talking about these days is technical debt. This is usually treated as a bad thing, as if it would not occur if only we had been more attentive during our design or programming stages, or spent more time getting better specs from our clients. This is then accompanied by a desire to throw everything out and start from scratch. While technical debt certainly can accumulate in these ways, I feel that these discussions often miss the point. A lot of the things that happen that create technical debt are not only foreseeable, they are virtually inevitable in the lifecycle of any code project of just about any scale or complexity. If our current design and programming practices are so fragile that we need to throw everything out and start again after these kinds of inevitable things happen in our code, then perhaps it is time to look at changing how we construct code instead of falling apart once that code has fallen out of some arbitrary definition of elegance. I wanted to write about a few different ideas in regards to this topic, but in this post I want to simply outline the kinds of inevitable changes that I think should be considered as normal parts of the development process and not as technical debt that somehow needs to be eliminated from a project.
Real-World Code Complexity
What do I mean by real-world code complexity? Well, I don’t mean cyclomatic/algorithmic complexity or even obfuscated C style complexity. What I mean by real-world complexity is the series of changes that a code base will necessarily go through its existence that will make every function grow by a factor of between 2x and 10x lines of code without actually doing anything it didn’t do before. It’s generally the kind of complexity that we can never account for in the design stage because there is simply no way to determine that it exists until we are there coding the details. Here is a non-exhaustive list of some examples:
- Dealing with third-party libraries that operate under a different design philosophy
- Discovering bugs in third-party libraries that require special-case work-arounds
- Discovering that some OS or third-party library function does not actually work the way it is described in the manual
- Error conditions that are only errors in some contexts but expected behavior in others
- Cross-platform libraries that function slightly differently between platforms
- Having one or more clients who have slightly different requirements from everybody else
- Having two teams or individuals working on different parts of a project who disagree on fundamental design philosophies
- Having to deal with a mixture of different error code return mechanisms from different libraries
Symptoms and Solutions
In general, a symptom that you have incurred this kind of technical debt is that you have functions that nobody wants to work on. You look at it, and despite the use of modern structured programming principles, it still looks and feels like spaghetti code. At this point, most people would say that the solution is to rewrite the code using a better design. However, I think first we should look at why our modern language and programming structures are unable to cope with this kind of imminently foreseeable and virtually inevitable evolution of a project. I feel that in some cases, our modern coding practices are purely designed around the idea of creating clean brand-new code, but are completely ill-suited to the evolution of code as it ages. The following are links to some articles I’m posting about specific issues. I’m going to add to these over time.