There is a kind of debt that never appears in the codebase. It is the distance between the code you own and the code you understand, and it stays completely invisible while everything is working. It reports itself for the first time on the day something has to be fixed, which is the worst possible day to discover how much of it you are carrying.
You are forward-porting a vendor kernel. It is two long-term releases behind, it carries somewhere over a thousand out-of-tree patches, and it has been shipping in a product for three years without trouble. A CVE is reported in a subsystem those patches touch, so the fix has to go in.
The port stops at patch 847. Not because anything is broken — nothing is broken. It stops because the patch conflicts, and the reason it exists is not written anywhere, and the engineer who wrote it left in 2021.
Everything needed to answer the question is present. The source is there, the history is there, the tree is one clone away. What is missing was never in the tree in the first place.
Technical debt is a decision somebody made. This is a decision nobody noticed making.
What the idea actually says
The name that has started to attach to this is epistemic debt: working software you own legally but do not own cognitively. Technical debt sits in the codebase. This sits in the engineer.
The two behave differently, and the whole difference is in how they get discovered. Technical debt is discoverable by reading. The awkward function is right there, the TODO is there, the eleven-minute build is there, and someone who has never seen the project can find all of it in an afternoon. It is visible continuously, which is exactly why teams manage it.
The other kind becomes visible under one condition only: something has broken, and the usual source of answers cannot help. Until that condition arrives, a system carrying an enormous amount of it looks identical to one carrying none. Same tests, same coverage number, same green build, same approvals.
You can see the difference in a single line of code. Take a spin_lock_irqsave() and write down why it is there. One version: it takes the lock and saves the interrupt flags. The other: local interrupts are disabled because this lock is also taken by the interrupt handler on this CPU, and without that the handler can try to take a lock this CPU already holds and deadlock us against ourselves; the _irqsave form exists because the caller may already have interrupts off, so the previous state has to be restored rather than interrupts unconditionally enabled.
The first sentence can be written by anyone who can read the line. The second cannot be written by anyone who does not hold a model of the locking context. Both are true. Only one of them is any use when the system is failing and you are the person on call.
There is no file to grep, no ticket, no build warning. There is only the day something breaks, and then you find out what you knew.
Why this matters more now, not less
Anything that produces working output faster than it produces understanding creates this debt. It is not new. Pasting an init sequence off a forum in 2009 created it. Inheriting a driver from someone who has left creates it. What has changed is the rate: a tool that writes a plausible page of code in four seconds moves the ratio between what you own and what you understand faster than any method before it, and it does so while every visible indicator improves.
That last part is the difficulty. The build is green. The review passed. The feature works. Every instrument you have says the work went well, and none of them is measuring the thing that is being used up.
One study has made the gap visible, and it is worth knowing about mainly for its design. Seventy-eight people built the same application in three groups — one unaided, one with an assistant, one with an assistant that would not let them merge anything until they had written down why the code worked. On the finished application the two assisted groups were indistinguishable. Then the assistant was taken away and a defect was injected into each person's own code, with thirty minutes to fix it. The group that had used the assistant freely repaired it at less than half the rate of the group that had been made to explain, and at a third of the rate of the group that never had one. It is a small study on novices, and its author says so. But nothing about the software recorded which group anyone was in.
Two groups shipped the same application. One could repair it and one could not, and no artefact anywhere recorded the difference.
The variable is not the tool. It is where the explanation gets required. The kernel worked this out a long time ago and wrote it into the process: Documentation/process/submitting-patches.rst requires you to describe the problem and justify the approach, and a changelog that only describes the diff is rejected. Every maintainer has been enforcing an explanation gate for thirty years without calling it one.
Which tells you where your own debt is sitting, and it is not upstream. Upstream extracts the explanation at the time, from everybody, on every patch, whether a person wrote it or a model did. It accumulates in the internal tree, in the vendor BSP, in the driver that ships in the product and never goes near a mailing list, because nothing there asks.
How to pay for it as you go
- Write the internal changelog as though it were going upstream. Not what changed — what was wrong, and why this approach and not the obvious one. If the sentence could have been written by someone reading the diff, you have written the useless version.
-
Gate the places where wrong code looks right. A plain
spin_lock()on a lock a hardirq handler also takes runs for months on a uniprocessor test board and deadlocks on the customer's SMP part. Adevm_allocation with a manualkfree()in the error path survives every test that never unbinds the device. None of these fail at review by inspection. -
Let the tools tell you what you did not understand.
CONFIG_PROVE_LOCKINGexists to report that your lock ordering was a belief rather than a fact. KASAN and KCSAN do the same for lifetime and concurrent access. They run on a schedule instead of depending on your discipline, which is their main advantage over everything else on this list. - Ask for the explanation before you ask for the code. It costs nothing and it changes what you are left with when the answer arrives.
- Break something you shipped and fix it unaided. Once a quarter. It is the only measurement of this that exists, and you can run it on yourself in an afternoon.
It is why we ask people to explain a patch before we treat it as finished. If you want that kind of practice with someone checking the explanation, that is what our Linux kernel and device driver programmes are built around.
Go back to patch 847. The company owns that kernel completely — the source, the history, the legal right, the revenue. What it does not own is the reason, and no amount of ownership produces that after the fact. The cost was taken on years earlier, on an ordinary afternoon, by someone who got something working and moved on. That is not a failure of diligence.
It is simply what this debt looks like at the moment you take it on, which is that it looks like nothing.
Ask yourself which of the things you shipped this year you could still repair with the assistant switched off.
Notice what that question is not. It is not an argument for using the tool less. The group in that study who used it freely produced good software, and so will you. It is a question about who is able to repair what you own. Technical debt can be paid down by anyone on the team, which is why it survives people leaving. This kind cannot. It is owed by one person, and the only way to settle it is to do the work that would have earned the understanding in the first place.
— Raghu Bharadwaj
Top comments (0)