On many teams I’ve advised or been a part of, code is generally viewed as an asset. Only some code, the “bad code”, is considered technical debt. The highest-performing teams, however, viewed things differently. For them, all code is technical debt on some level.
Programming vs. Software Engineering
Software requires two broad classes of effort. There is the immediate effort to write the software, and then future effort to maintain it. (Titus Winters of Google would call the former simply “programming”, and the sum total of both as “software engineering”).
Software engineering is programming integrated over time. Engineering is what happens when things need to live for longer and the influence of time starts creeping in.— Titus Winters
It turns out that both the initial effort (programming) and the eventual effort (software engineering) are hard to estimate. If you’ve been in the software world long enough, the notion of a project’s initial implementation being delivered on time is so rare, it’s almost a joke. And of course, as you get to the “future effort” part, things become even harder to estimate and predict.
A Taxonomy of Technical Debt
Once you’ve built something, you (or someone else) become probably responsible for maintaining it. This cost is usually referred to as “technical debt”. We can break the cost of this future work into two broad classes:
- Interrupts: Interrupts are when existing systems are taxing your time through reactive work like fixing bugs, fire-fighting, etc. Writing code now that creates interrupts in the future means you (or someone else) will be able to spend less time on making progress on other work later. Both the quantity and severity of interrupts matter. Interrupts are particularly hazardous to engineering teams because they are hard to plan for and usually result in forced context switching.
- Inertia: Inertia means that a system is hard to make new changes to (because it is hard to understand or reason about, because it’s brittle, not modular and hence hard to change, and so on). This makes forward work difficult for you (ie even when you can spend time doing forward work, it’s really slow) or for others (e.g. because the system is hard to understand, it is a tax on the time of people who need to learn more about it, as well as on people who need to explain to others how it works).
It’s worth noting that if you had two systems that were identical in quality, you’d find that the costs of interrupts increased with system usage—how many people use your product, how often they use it, and how diverse their usage patterns are. In fact, Hyrum’s law tells us that the more people use your product, the more diverse their usages will be. But with the pressure of increased and different usage, your system finds new and different ways to fail, and the cost of failure (since your system is highly-used and depended upon) increases, too.
On the other hand, the cost of inertia increases with the quantity/scope of future changes you need to make to your product. And, of course, for poorly-designed systems, inertia and interrupts create vicious feedback loops. High inertia means you create bugs as you change your code, resulting in interrupts. And when interrupts happen and you need to fix them, it will be really costly because your system has inertia.
The point here is that all software is costly. Poorly written software is obviously more costly, but all software requires effort both now and in the future. Hence, it all creates technical debt—you will be paying interest on it, and occasionally you (or someone else) may need to pay down the principal with some refactoring or re-architecture. In other words, you are creating a liability.
A lot of people think of liability from a financial (debt) or legal perspective, but literally, a liability is simply “the state of being responsible for something”. And when you write software, you or someone else will be responsible for it.
Why Write Code?
But if all code is technical debt, why write any software at all? Well, the functionality that software enables is an asset. At some level, any valuable piece of software is solving some problem. And that’s the important distinction here. Software is the means, not the end.
Another way to frame this comes from “Uncle Bob” Martin (author of the seminal book Clean Code—though this framing is covered in another of his books, Clean Architecture). He views software as having two dimensions: behavior and structure.
Every software system provides two different values to the stakeholders: behavior and structure. Software developers are responsible for ensuring that both those values remain high.— Bob Martin
Uncle Bob goes on to argue that structure (the ability to modify a piece of software) is more important than behavior (its current functionality). His argument is compelling: a perfectly functional but inflexible system will be impossible to change when change is required, but a poorly functioning system that is extremely flexible can easily be fixed.
I think that statement is mostly true. I use the word “mostly” because you could argue that there are some systems where existing functionality matters more than future flexibility. There are some critical contexts in which being absolutely certain that software is operating correctly outweighs any increased costs in flexibility (e.g. a car, an airplane, medical equipment). And there are some contexts in which not having some functionality really quickly will mean that it doesn’t matter how flexible that software is in the future, because it will have no future (e.g. an early-stage startup). But I don’t want to go too deep on this topic, since it’s tangential to the point I’d like to get to (and we’re getting there, I promise!).
Future Scope and Likelihood
So, putting this all together, software makes sense to build if and only if the value it creates now (through its functionality), and the value it enables in the future (through its ability to change its functionality), outweigh the costs it takes to build it now and maintain it in the future. But that “future” part is hard to predict.
I’ve always been amazed at how financial analysts can put together a spreadsheet to value an asset or investment. They’ll confidently forecast out a series of cash-flows, often in perpetuity. When they can’t forecast perpetuity with a straight face, they’ll slap a terminal value on it instead.
In software, it’s not that easy to predict (or pretend to predict) the future. So it’s easy to just, like Bob Martin, say that flexible software is better than inflexible software. But that’s a truism and it doesn’t really solve the problem of how, exactly, to think about building your software.
Software purists will make the case that “good software is good software”. Practice good design patterns, use the SOLID principles, remove (or encapsulate) complexity, etc. And you should. There are things that are almost universally good or bad architectural decisions in software, and we’ve got some great literature to help guide us.
But remember, writing good software is the means, not the end. Your goal is to build software in a way where current and future functionality outweigh current and future costs. And to get that right, we have to understand the scope and likelihood of future changes. Without that, we’re flying blind.
Domains, Users, and Problems
I’m a big fan of Domain-Driven Design because it shifts focus off of the code into the domain you’re trying to model.
The most significant complexity of many applications is not technical. It is in the domain itself, the activity or business of the user. When this domain complexity is not dealt with in the design, it won’t matter that the infrastructural technology is well-conceived.— Domain Driven Design
The promise is simple: model the underlying domain correctly, and not only should the code and architecture fall into place now—they should be able to adapt and evolve as your requirements change.
Deeply understanding the domain you’re working with is a great start, but focusing too much on domain modeling can be misguided. Your users don’t care about how the domain is modeled—they care about whether your software solves their problems. So you actually need to understand three things:
- The domain.
- Your users.
- Your users’ problems.
How to actually do that probably requires a separate article(s), but it basically comes down to spending time thinking about, discussing, and analyzing those three aspects.
As a final thought, there’s a risk of taking “software as a liability” to an extreme.
You’ve probably worked with one of these developers before: the type that gatekeeps software. Asked to implement something by a Product Manager or a colleague, their go-to response is “no, that’s too complicated”. Then they walk off feeling good that they have just prevented adding a bunch of complexity into the code base, and the future stream of liabilities that would create.
Any principle can be abused with the wrong attitude. So yes, all software is a liability, and it all has costs, but by truly understanding the domain you’re building in, the users you’re building for, and the problems you’re solving, you can help manage the trade-off between the cost of software and the benefits it provides.