Technical Debt Is Specification Debt
The term has lost its meaning
Ward Cunningham coined “technical debt” in 1992 as a metaphor for the consequences of shipping code that you know isn’t quite right. Like financial debt, it’s a deliberate tradeoff: you take a shortcut now, and you pay interest on it later. The interest is the extra work required to maintain, extend, or modify code that was built with known compromises.
It was a useful metaphor. Now it’s a catch-all.
Teams use “technical debt” to describe everything from outdated dependencies to messy code to architectural decisions they disagree with. It’s invoked to justify rewrites, to argue for refactoring time, and to explain why things take longer than expected. It’s become so broadly applied that it explains everything and diagnoses nothing.
When everything is technical debt, the term stops being useful. You can’t prioritize it, because it describes too many unrelated problems. You can’t measure it, because there’s no shared definition. You can’t pay it down systematically, because you don’t know what you’re actually paying for.
The problem isn’t the metaphor. It’s that the industry is pointing at the wrong thing. The debt isn’t in the code. It’s in the specifications, or the absence of them.
The real debt
Here’s what actually happens when technical debt accumulates.
A feature gets built. The requirements were discussed in a meeting but never written down in detail. The engineer interprets them, makes judgment calls on edge cases, and ships the implementation. The code works. Nobody goes back to document what was actually built, what the edge cases are, what the implicit business rules are, what the performance characteristics are.
Six months later, another engineer needs to modify the feature. The original engineer has moved to a different team. The requirements from the original meeting are in a closed ticket with three bullet points. The only complete description of the feature’s behavior is the code itself.
The new engineer reads the code. They reverse-engineer what it does. They make their change. They introduce a bug because they didn’t understand one of the original edge cases, an edge case that was never documented, never specified, only encoded in the implementation.
That’s not a code quality problem. The original code may have been perfectly clean. The new code may be perfectly clean too. The problem is that the specification was never written. The knowledge of what the software is supposed to do was never captured in a form that’s independent of the code. When the original engineer left, the specification left with them.
This is specification debt: the accumulated cost of building software without maintaining explicit specifications of what it does.
Legacy code is code without specs
The industry’s definition of “legacy code” is revealing. Michael Feathers defined it as “code without tests.” But that’s a symptom, not the disease.
Code without tests is code without a verification mechanism. But verification requires a specification, you need to know what the code should do before you can test whether it does. The reason legacy code doesn’t have tests isn’t that nobody thought to write them. It’s that nobody maintained the specification that the tests would have been derived from.
Legacy code isn’t old code. Plenty of old code is well-understood, well-tested, and easy to modify. Legacy code is code where the specification has been lost, where the only way to know what the software does is to read the implementation and hope you understand it correctly.
You can have legacy code that’s six months old. A feature built without specifications, modified twice by different engineers, with no documentation of the intended behavior. That’s legacy code. The age of the code is irrelevant. What matters is whether the specification exists.
How the debt accumulates
Specification debt compounds the same way financial debt does. The interest payments get larger over time, and eventually they consume the majority of the team’s capacity.
Phase 1: Tolerable. The team is small, the codebase is new, and the original engineers are still around. They carry the specifications in their heads. When someone has a question, they ask the person who built it. The debt exists; the specs aren’t written down. But the cost is manageable because the knowledge is accessible.
Phase 2: Growing. The team grows. The original engineers get promoted, move to new projects, or leave. New engineers join and inherit code they didn’t write. They spend increasing amounts of time reading code to understand what it does. Onboarding takes longer. Modifications are riskier. The knowledge that was in people’s heads is now scattered, incomplete, or gone.
Phase 3: Crushing. The codebase is large, the team has turned over multiple times, and the specifications were never written. Every change requires archaeology: reading code, tracing execution paths, checking git blame, and hoping that the commit messages explain why, not just what. Simple features take weeks because nobody is confident they understand the system well enough to change it safely. The team spends more time understanding existing code than writing new code.
This is what teams call “technical debt”, and they’re right that it’s expensive. But the debt isn’t in the code. The code is fine. The debt is in the missing specifications that would make the code understandable, verifiable, and safely modifiable.
Refactoring doesn’t pay the debt
The standard prescription for technical debt is refactoring: clean up the code, improve the architecture, reduce complexity. This helps with code quality problems, genuinely messy or poorly structured implementations, but it doesn’t address specification debt.
You can refactor a module from spaghetti into clean, well-structured code and still have no specification for what it does. The code is prettier. It’s no more understandable in terms of intent. An engineer reading the refactored code can see what it does more easily, but they still can’t see why it does it, what the expected behavior is under all conditions, or what business rules are encoded in the logic.
Refactoring without specification is rearranging deck chairs. The code looks better, but the knowledge gap remains. The next engineer who needs to modify it will still be reverse-engineering intent from implementation.
Paying down specification debt means writing the specs. It means going back to the existing implementation, extracting the behavior, documenting it in structured, verifiable terms, and validating that the specification accurately describes what the code does. Once that’s done, the specification becomes the reference. Future changes are made against the spec, not against an interpretation of the code.
Preventing accumulation
A spec-first development process prevents specification debt from accumulating in the first place.
When every feature starts with a specification, a structured description of the intended behavior, and the implementation is validated against that specification before it ships, the knowledge is captured at the moment it’s cheapest to capture. The engineer writing the spec has the full context. The business rules are fresh. The edge cases are top of mind.
The specification is then maintained alongside the code. When the behavior changes, the spec is updated. When a bug reveals a gap in the spec, the spec is patched. The specification and the implementation evolve together, and the gap between them is kept to zero.
This isn’t free. Maintaining specifications takes effort. But it’s dramatically less effort than the alternative: letting the specifications decay and paying the compounding cost of ignorance for every modification, every onboarding, and every migration for the life of the codebase.
What you’re actually paying for
The next time someone on your team says “we have a lot of technical debt,” ask a different question: do we have specifications for the components that are hardest to work with?
If the answer is no, the problem isn’t the code. The code might be perfectly adequate. The problem is that nobody knows what it’s supposed to do, not precisely, not verifiably, not in a form that a new team member can read and understand.
That’s the debt. The code is just where it shows up.