Over the last year I have spent a lot of time making calculations about code quality using different metrics and different combination of metrics. The goal was to be able to identify which parts of the code base were going to cause problems later on. Very often this is referred to as technical debt. Recently I came across this article about what the level of technical debt should be. The common point here is that people talk about technical debt as if it were something that can be quantified, expressed as some value x. To a certain point you can define metrics that indicate bad code, but any attempt to define what x is invariably ends with a definition something like: x = (y + z) / a.
This obviously adds no real value, and people are left with the unsatisfactory result that technical is something that is there and at some point will bring your project to a halt if you don't continually bring it down. But how are you going to reduce something that you can't quantify. If anyone remembers the film Amadeus, there is a scene where the emperor tells Mozart that there are too many notes in his opera. Mozart asks him which ones he should take out. It's the same thing with technical debt. Talk about how the technical debt needs to be at a certain level, assumes that this can be quantified and also that you can make a conscious decision about how to reach that level. For any application where technical debt is even an issue, it is equivalent to knowing which notes to take out to improve the opera.
However if you look at this video with Ward Cunningham, technical debt is something completely different. He describes it more as a sub-optimal implementation. Not sub-optimal because of bad code, but because the full picture is not yet known and a shortcut has to be taken. It can also be sub-optimal because you want to launch earlier and are willing to release a less than perfect result. With this definition technical debt is not related to bad code, but related to implementing an imperfect solution.
Looking at technical debt from a software architecture perspective makes sense, because architecture is about deferred decisions. When you design a solution you make some conscious choices about which decisions you wish to be bound by now and which decisions you wish to have the option of changing at a later stage. (If you aren’t making conscious decisions you may stop reading now and go back to adding more technical debt).
If we consider technical debt as a decision to go with an implementation and keeping the option to change it later, then managing technical debt becomes a question of managing change in your application, i.e. how long does it take you to exchange one implementation for a better one? This is also known as modifiability. This is something that is something that is fairly easy to measure. Ask your team to swap one implementation for another and measure how long it takes.
Does this mean that all the people talking about technical debt as bad code are wrong? No, they are not wrong. Bad code is also a drag on your development. When you measure how long it takes to modify your application, then you should also be checking what obstacles the team encounters. Bad code is a classic obstacle, because it makes it difficult to understand or work with the current implementation. But what if the bad code is in a well isolated component? Then you can easily throw away that component and not worry about the bad code. Code should be cheap, the expensive part should be determining what to code.
Agile as an excuse for short term decisions
"That's not a bug, it's a feature" - also known as: "I implemented it like this because nobody specified that it should work differently". This is a classical start down the road of technical debt. All changes need to be considered in a bigger picture, and should be the result of a discussion between the product owner and the application architect (as well as the development team). By blindly pursuing short term gains the team (here team includes both business, architecture and development) will not be aware of the long term consequences of its decisions. This leads me back to the point I made earlier about the conscious decision making and being aware of which things will need to change in the future. In the past I've used the comparison of going on a trip and hoping to get to a good destination by simply turning left and right enough times. It may work, but most people will agree that you are better off having a clear understanding of where you are going. This way you can break down the journey into the big picture routes and defer knowledge of the minor deviations that will be needed underway. Agile methodologies have a lot of advantages, but should never be used as an excuse to not have a big picture in mind.
Going back to Ward Cunningham's video, bad code should not be tolerated. However technical debt has to be seen in the context of the current place in the life cycle of the application itself. Technical debt is never a question of looking at a snapshot of the system, it only makes sense when looked at in a repayment scenario.
Managing technical debt becomes a question of adhering to some well established development practices and focusing on the application life cycle.
- Be clear about what you are implementing. Don't rush off and write code. Talk about a feature until it is fully understood.
- Write detailed acceptance criteria, preferably as BDD style acceptance tests.
- Always write unit tests. This is in relation with the point above. You should always know that your system runs.
- Be disciplined about actually throwing away spike code. Spike code should be a support for any feature discussion, not a replacement.
- Have a tightly knit team that know each other. This reduces miscommunication and promotes a common understanding of the system.
- Check that what is checked in is also what is supposed to be checked in. Here I am not only talking about code reviews, but also about structure reviews.
If any of the points above causes your development progress to be unacceptably slow then you need to have a look at the team and organization instead of cutting corners. As part of such a review, you should have metrics about:
- How often do specifications change? I.e. is there a problem about how well you define your features? Do you know your domain well enough?
- How long time does it take to replace a piece of code / feature? This is how you know how difficult it is to modify your system.
- How quickly can you verify your system? You will need to be able to quickly and reliably verify that an implementation is done correctly (i.e. according to current specifications).
If your specifications change often, or continuously need to be detailed further, then you must have code that can quickly be modified. Conversely, if change is difficult, then you need to be very careful about your specifications. For both cases you will need to run your tests before you can sign off a feature as done.
In support of the above metrics, it is useful to have the standard code metrics (maintainability index, cyclomatic complexity, coupling and lines of code) because they give you a measure of how well your code is structured. Deviations in any of the metrics is often a symptom of a feature that has been developed without a clear understanding of the requirements, or has grown to accommodate too many requirements. So code metrics can be useful, but only if they are used as markers to highlight other problems. This way they serve as a basis for a review of the features the code is used in.
It can also be that you need to look at the skills of your development team! You may be suffering from innovation debt.