Disclaimer: this blog post was triggered/inspired by an article you can find here. I strongly encourage you to get familiar with it - whatever I put below is built on it as a foundation, so all the kudos go to The HFT Guy.
Mental models are extremely powerful thought constructs. They do guide & simplify our reasoning, but they also prime us. Bias us. And when treated as axioms, dangerously narrow down our thinking horizons.
All of the above applies to the popular concept of "technical debt" (TD). I won't cover its meaning and history (there are tons of online materials on those) - instead, I'd like to focus on why the concept of TD is far less useful than we tend to think and actually quite harmful for our daily work.
Why so? Mainly because:
- there's no code w/o technical debt - TD is a "local" maintainability factor (a multiplier - that can span from very low to extremely high values) for a particular piece of a system; hence it's not separable from good (/healthy) code; consider it an inevitable "property" of any code
- mentally dividing work between "fixing/fighting TD" and "developing features" usually leads to: never doing anything about TD and just going short-cuts all the time ("I'll just do it the quick'n'dirty style, we'll need to refactor all that anyway ... some day")
- we tend to brand as TD everything "we don't like at the moment" (a code written by someone else, a code that uses a library we find passé atm, etc.) - which leads to several types of immature & unprofessional practices and massive wastes ("lava flow" effect, stacking unnecessary abstractions, etc.)
- for the reasons stated above, TD is highly subjective and prone to individual judgment, hence nearly impossible to reliably quantify & measure (regardless of whether you estimate the cost of its reduction or its actual impact on the development process); but it looks like a great candidate for a universal excuse (and it's frequently used that way)
- it conveniently puts engineers in "we VS the debt" mode by giving the common enemy (that won't speak up) - some ephemeral "entity" you can stand in opposition to; this detail is important: no-one ever openly admits to the ownership over the TD, it just pops up out of the thin air and hinders the work of poor engineers ...
Nevertheless, peeling TD out of its name won't make all its negative consequences disappear, right? Naming it explicitly at least makes discussing it (among engineers) easier & (hopefully) more conclusive.
Embrace your TD
But maybe there's a better way to tame it? E.g. by ...
- ... treating it as something inevitable (as there's no and there'll never be perfect, debt-less code)
- ... containing it within component/element/unit boundaries
- ... and coming up with a way to "reset" it (once in a while) within those local boundaries?
I've already written (over four years ago) about some particular concept that can help with that - the immutable code. It may sound weird at first glance, but apparently many companies adopt a similar standpoint:
- they create software with the EXPIRY date (on the level of module/component/piece) and re-create it every few years (because business/scale conditions do change anyway) - as a normal business-as-usual activity
- they optimize not for "perfect architectures" but for future re-writes on the level of module/component/piece (e.g. by enforcing explicit contracts - so after a single component replacement, the whole system doesn't collapse like a house of cards)
That's (creating with a re-write in mind) actually something I've already covered in the different post in the past, so feel free to reach there for more details.
The (true) catalyst of TD
Great! But what if my system wasn't written in a re-write-friendly way? What if there are no explicit contracts, dependencies are out of control, the only units of abstractions are the language's syntactic constructs (a flat list, w/o hierarchy or varying perspectives) and it's not possible to map certain capabilities/duties?
HA! And now we've reached a conclusion I was looking for - the main pre-requisite to build a future-proof solution with a TAMEABLE technical debt is ... by doing a deliberate design. It can be done up-front or in an evolutionary way - the mode doesn't matter that much. The truly crucial elements are:
- capability-driven composition (driven by purpose, function) with explicit boundaries and dependencies
- the ubiquitous language used everywhere
- wide (expressive) but shallow (w/o internal details) contracts treated as a binding promise to all the consumers
That's also why so many attempts to reduce technical debt by purely technical refactoring (e.g. splitting big classes, harmonizing conventions, simplifying the flow in complicated procedures) are a pure waste w/o any noticeable positive long-term effect.