Toward a Galvanizing Definition of Technical Debt
Everyone involved in software development seems to know what Technical Debt is. It’s a powerful metaphor. In Ward Cunningham’s original formulation, Technical Debt was the accumulated distance between your understanding of the domain and the understanding that the system reflects. We all start out with some understanding of a problem, and we write code to solve that problem. But, we learn as we go. If the code doesn’t keep up with that learning we continually stumble over a conceptual gulf when we add new features. The cost of adding features becomes higher and higher. Eventually, we simply can’t.
If this definition sounds unfamiliar, or a bit different than what you’ve read before, it’s probably because Technical Debt has become conflated with another concept - general systems entropy. It’s easy to write code quickly and not pay attention to good factoring. Over time, all of these small omissions of care accumulate and we end up with code that ends up looking more like a jungle than a clean understandable guide to the behavior of a system.
Some people have tried to reconcile these differences by naming several different types of Technical Debt, but I think that what has happened is very much like what happened with the idea of Mock Objects. The paper that introduced Mock Objects into software development was very clear that mocks were testing objects that you can set expectations on in advance, but at the time the paper was released there was no nomenclature around stubs, fakes, or doubles, so people used the word mock to describe them all. And, that was fine for the time, but language and terminology evolve.
The roughest thing about Technical Debt is that people assume it is inevitable. We know that refactoring is the antidote, but despite this we still end up with Technical Debt. Beyond that, the use of debt as a metaphor can lead to some problems if it is taken too literally. It isn’t uncommon to see tools that claim to give you a monetary amount for the Technical Debt in your system. And, superficially, it seems like a reasonable thing to calculate. You identify design problems and estimate how much time it takes to fix them.Then you multiply that amount by the hourly rate of a developer. You end up with a figure. A tool may tell you that your code base has 347,734.12 USD of technical debt. That’s fine, but what do we do with that number? How relevant is it? The answer is - not very.
The first problem is that design quality is hard to measure in any meaningful way. We can measure surface things like adherence to various design standards or deeper things like general coupling and cohesion, but the things that make change easier or harder are elusive. Moreover, they are situational. A new feature may be very easy to add to your system, or it may trigger a lot of rework. While good coupling and cohesion make change easier in general, a lot depends upon the specifics of the new features coming in. This leads us to the other problem with quantifying Technical Debt. Not all design problems are equal. In any code base there’s a lot of code that will never change. Some code is “hot.” It changes continually, and the “hot-ness” of an area of code changes over time. We can try to devise a formula that takes all of this into account, but again, once we do,what does it do for us?
For a while, I’ve been using a different definition of Technical Debt. It helps teams frame their work in a way that highlights their choices and it can lead to better ones.
Technical Debt is the refactoring effort needed to add a feature non-invasively
There are a couple of different things about this definition. One is the emphasis on refactoring prior to adding a feature. For many people, refactoring is clean up work. They add code to add a feature, look at the result and then say “this could be better.” Then they refactor. We can gain a lot of advantage by turning this around. We can look at the current state of the code when we’re thinking of adding a feature and ask ourselves what the code would have to look like to make it easier to add a particular feature. Quite often it’s a bit of generalization, renaming or a clarifying extraction. We can alter the structure a bit and make it ready for the change.
The second interesting part of the definition is that it defines Technical Debt in relation to features. There’s no way to measure the current design of an entire system against some ideal of good design and arrive at a misleading number. We just need to care about how much refactoring we need do to end up in a place where we can add a feature in a good way.
The third part of the definition is the one that requires the most explanation - the effort to be able to add a feature non-invasively. What does this mean?
public class Scheduler {
public void addEvent(SchedulingEvent event) {
events.add(event);
display.show(event);
store.save(event);
}
...
}
In this code, we have a method that does a few things. Let’s suppose that we were asked to have it do another. Whenever we receive a scheduling event, in addition to adding it, displaying it, and saving it, we should send it to a peer system also. How can we make this change? It’s very easy. We just add another line to the code:
public class Scheduler {
public void addEvent(SchedulingEvent event) {
events.add(event);
display.show(event);
store.save(event);
peerNotifier.notify(event);
}
...
}
But, is this making the code any better?
One of the most important principles of good design is the Open/Closed Principle. There are many formulations of it but the gist of it is that our code is better to the degree that we don’t have to change it much when we add features. We should be able to make modifications primarily by adding new classes and functions rather than changing existing ones. The Open/Closed Principle dovetails nicely with the Single Responsibility Principle. If an abstraction has a single focused responsibility, adding another one is rarely the best choice. When responsibilities are tangled in code, it’s hard to change one thing without inadvertently changing something else.
The problem with our modification should be clear now. We’re adding another responsibility to a class that is already concerned with too many different things. Schedulers should be about scheduling, not logging, displaying, or sending information to peers. What would it be like if we refactored the original to make it possible to add our feature non-invasively.
class Scheduler {
public void registerEventListener(SchedulingEventListener listener) {
listeners.add(listener);
}
public void addEvent(SchedulingEvent event) {
events.add(event);
for(SchedulingEventListener listener : listeners) {
listener.eventAdded(event)
}
}
...
}
In this code, we we can register listeners to event addition whenever we like. The addEvent method runs through the list of registered listeners and gives them each the event we’ve received. We can easily register one listener that logs, another that displays, and another that notifies peers - and in each of these cases, the code in Scheduler does not have to change. In the language of the Open/Closed Principle, it is closed to modification. We had to do a bit of work to refactor the code to this state, but once we have, adding the feature is trivial, and non-invasive.
It’s easy to challenge this example. It borders on over-engineering. With more context we might do more extensive refactoring to make the addition of the new feature easy and non-invasive without introducing listeners, but the point is that all of these discussions come to the forefront when we see Technical Debt in this slightly different way. We have conversations about how the code should be to accept change easily, and that conversation can be galvanizing.