Technical debt gets a bad name. In most organisations, calling something “tech debt” is an accusation: somebody did something wrong and now we’re paying for it. The original metaphor is more interesting. Ward Cunningham, who coined the term, was describing a deliberate choice: ship something you know is imperfect, learn from the market, and pay back the debt with the knowledge you’ve gained. Real codebases carry both kinds: deliberate loans that bought time to learn, and accidental debts that nobody noticed until they started compounding.
The original metaphor
Ward Cunningham introduced the debt metaphor in 1992, and it’s worth going back to what he actually said, because the industry has mangled it beyond recognition.
His point was this: sometimes you ship code that you know doesn’t perfectly reflect your understanding of the domain. Not because you’re lazy, but because your understanding is still developing. Shipping imperfect code is like taking out a loan: you get something now (working software, market feedback, learning) and you pay it back later (refactoring, redesigning, rewriting) once you understand the domain better.
The critical word is “deliberate.” You know the code is imperfect. You know why it’s imperfect. You have a plan, or at least an intention, to come back and fix it. The debt is a strategic choice, not an accident.
This gets confused constantly. People use “tech debt” to mean:
- Code that’s badly written (that’s not debt, that’s just bad code)
- Tests that nobody wrote (that’s negligence, not a loan)
- A system that’s grown organically without design (that’s accidental complexity, not a strategic choice)
- Features that were shipped under pressure without time to do them properly (that might be debt, depending on whether anyone noticed what they were trading away)
The distinction matters because the response is different. Deliberate debt needs a repayment plan. Accidental debt needs a discovery process; you have to find it before you can fix it. And bad code just needs someone to care enough to improve it.
The week-one build
A common pattern: the developer who joins a startup in week one builds a complete subscription system in an afternoon, with the LLMA neural network trained to predict the next token in a sequence, large enough that it generalises to tasks it wasn’t explicitly trained for. generating most of the code. (The Greenbox version of this is a worked example.) The system assumes fixed product, flat pricing, no pausing, no skipping, no substitutions. Within weeks, every one of those assumptions turns out to be wrong.
Was this technical debt? Yes, but it was the good kind.
The developer didn’t know the assumptions were wrong when they built the system. Nobody did. The team hadn’t done any discovery yet. The founder had a picture in their head, the developer built to that picture, and the picture turned out to be incomplete. The system was wrong, but shipping it accomplished two things: it got the product to market, and it revealed what “correct” actually looked like through customer feedback and the discovery workshops that followed.
Here’s the nuance. If the developer had known the assumptions were wrong and shipped anyway, that would be deliberate debt: a loan taken with eyes open. What actually happens in most week-one builds is closer to accidental debt: building on assumptions that turn out to be false. But the outcome is similar to Cunningham’s model: you learn from the market, and the learning tells you how to pay it back.
Deliberate or accidental matters less than what happened next. Did the team recognise it? Did they plan to address it? Or did they pile more features on top and hope nobody noticed?
The first Event Storm on a young codebase is usually the moment the debt becomes visible. The whole domain goes up on the wall. The mismatches between the code and the actual business process become impossible to ignore. The system’s assumptions get challenged, documented, and prioritised for rework. That’s debt management.
Deliberate debt: launch with the manual version
A cleaner example of deliberate debt is the manual-process pattern. The business needs a complex decision to happen (substitutions, matching, routing, scheduling) and the team has two choices.
Option A: build an algorithm that automates it from day one. Weeks of development. Complex domain logic. High risk of getting it wrong because nobody yet understands the rules well enough to encode them.
Option B: a domain expert does the decisions manually. They know the constraints, they know the customers, they make the calls. It takes hours every week. It doesn’t scale. But it works now, and the team learns what “good” actually looks like by watching the expert do it.
Option B is deliberate debt in its purest form. The team knows the manual version won’t scale. They know they’ll need to automate eventually. But automating a process you don’t understand is how you get an algorithm that makes terrible decisions very efficiently. Better to do it by hand, learn the patterns, then automate with confidence.
There’s a political complication that comes with this pattern. The expert who does the manual version becomes a bottleneck, and discovers that being a bottleneck is also being indispensable. When the team eventually moves to automate, the conversation isn’t just about efficiency; it’s about identity. The deliberate debt has bought time for the domain to be understood, but the repayment is harder than it looked because the human in the loop is now invested in the loop. (Worth knowing going in: this is the cost of the loan.)
The monolith: accidental debt at scale
The week-one build was wrong-on-purpose. The accidental monolith is wrong by accident.
As a team grows from five to fifteen, the codebase grows with it. Features get added where it’s convenient, not where they belong. The billing module talks directly to the delivery scheduler. The allergen flags get tangled into the product catalogue. The substitution logic ends up scattered across three different services that nobody can explain.
This is the kind of debt Cunningham wasn’t describing. Nobody sat in a room and said “let’s build a monolith and fix it later.” The monolith emerged incrementally, one feature at a time, each one making sense in isolation, none designed with the whole in mind.
This is also where Domain-Driven Design earns its keep. Bounded-context mapping makes accidental debt visible. “Subscription,” “Delivery,” “Catalogue,” and “Billing” are separate domains with different reasons to change, but in the code, they’re intertwined. A change in billing breaks delivery. A change in the catalogue affects substitutions. The coupling is invisible until someone draws the boundaries.
The debt isn’t strategic. It isn’t a loan. It’s the natural consequence of a small team building fast without a model of how the pieces should fit together. That doesn’t make it a crime; every growing codebase accumulates this kind of complexity. But it does make it harder to repay, because nobody documented where the bodies are buried.
Martin Fowler extended Cunningham’s metaphor into a useful quadrant. On one axis: deliberate versus inadvertent. On the other: reckless versus prudent. A well-written week-one build that turns out wrong is inadvertent and prudent: the code was good given what was known. An accidental monolith is inadvertent and, well, not reckless exactly, but certainly not prudent. Nobody intended to create it. Nobody was even thinking about whether the code structure matched the domain structure. That’s the quadrant where accidental debt accumulates fastest, not because anyone was careless, but because the question of where code should live was never asked.
DDD’s bounded contexts give a team the vocabulary to ask that question. Once you can say “this logic belongs to Billing” or “this belongs to Delivery,” you have a rule for where new code should go and a criterion for evaluating where existing code is misplaced. The vocabulary prevents new accidental debt, even as you’re still paying down the old stuff.
The debt conversation
One of the hardest parts of managing technical debt is talking about it. Developers know it’s there. Product managers hear about it in sprint retrospectives. But the conversation often stalls because the two sides speak different languages.
Developer: “We need to refactor the subscription module.”
Product manager: “What does the user get from that?”
Developer: “Nothing. But we get the ability to ship changes faster.”
Product manager: “I have twelve feature requests. ‘Ship changes faster’ doesn’t sound like a priority.”
This conversation happens in every product team, and it’s almost always unproductive. The developer frames debt in technical terms (coupling, test coverage, code quality). The product manager frames priorities in user terms (features, stories, outcomes). Neither is wrong. They’re just talking past each other.
The reframe that unlocks it: don’t tell me you need to refactor. Tell me what it costs to not refactor. How many hours did the last change to that module take? How many hours would it take if the code were clean? What’s the difference, per change, multiplied by the number of changes per quarter?
Run the numbers. Five changes last quarter, four days each. With clean code, two days each. Ten developer-days per quarter saved. At a typical loaded developer cost, that’s roughly $15,000 per quarter in productivity that the debt is currently consuming.
Now the product manager can prioritise refactoring against features. Is feature X worth more than $15,000 per quarter in ongoing productivity gains? Maybe. Maybe not. But now it’s a decision, not an argument.
The debt conversation works when both sides share a language. That language is almost always cost (not technical cost, but business cost). How much does this debt slow us down? How much does it increase the risk of incidents? How much does it cost in developer frustration and turnover? When the debt has a price tag, the prioritisation becomes a normal business decision instead of a religious debate.
Event Storming as debt prevention
One of the quieter contributions of the discovery techniques was catching debt before it became debt.
A typical example: during an Event Storm, a team discovers a timing mismatch in billing. The subscription system charges customers on signup day. But fulfilment runs on a weekly cycle. A customer who signs up on a Thursday is charged immediately, but their first delivery isn’t until the following Tuesday: six days of paying for something they haven’t received.
This would have been debt; bad debt, the kind that generates support tickets and chargebacks. The Event Storm catches it before a single line of code is written. The flow gets redesigned: charge on delivery, not on signup. The hotspot on the wall turns into a design decision, not a bug.
Example Mapping did the same thing at a finer grain. Every red card in an Example Mapping session is a question the team hasn’t answered yet. If those questions go unanswered into code, they become assumptions, and assumptions that turn out to be wrong are the most common source of accidental debt.
The red cards don’t eliminate debt. They make it visible before you take it on. And visible debt is manageable debt.
There’s a broader point here about the relationship between discovery and debt. Every discovery technique in the modern playbook has a debt-prevention function, even if that’s not its primary purpose.
Event Storming prevents domain misunderstanding debt: the kind where the code models something differently from the business.
Example Mapping prevents specification debt: the kind where edge cases aren’t handled because nobody thought about them.
DDD prevents structural debt: the kind where components are coupled for accidental rather than essential reasons.
Decision tables prevent logic debt: the kind where business rules are approximated rather than fully specified.
None of these techniques were designed as debt prevention tools. But debt is, at its core, a gap between what the code does and what it should do. Discovery techniques close that gap before the code is written. Every gap they close is a debt they prevent.
ADRs as debt documentation
When the team started writing Architecture Decision Records, they got something unexpected: a register of known debt.
An ADR doesn’t just record what was decided. It records what was considered and rejected, and often includes a “consequences” section that says things like: “This approach will need to be revisited when we support multiple cities” or “This trade-off limits us to 5,000 subscribers before we need to redesign.”
Those consequences sections are debt documentation. They’re the team saying, explicitly, “we know this will need to change, here’s when, and here’s why.” When a new developer joins and asks “why does the delivery scheduler work this way?” the ADR tells them: it was built for one city, we knew it would need redesigning for multi-city, and here’s the boundary condition that triggers the redesign.
Compare that to the alternative: a system that works until it doesn’t, with no documentation of why it was built that way or when it’s expected to break. The debt is the same. The difference is whether you have a map of it.
Real ADRs are never perfect. Some are too terse. Some get written after the fact, when the decision has already faded from memory. But the habit of documenting known limitations, “we’re taking this debt deliberately, and here’s the trigger for repayment”, means the team can make informed choices about when to pay it back.
The feature factory as a debt factory
There’s a pattern most growing teams hit: the feature factory. The team ships features (lots of them) without measuring whether any of them matter. Each feature adds code, adds complexity, adds maintenance burden. Nobody can say whether the ongoing cost is justified because nobody measured the impact.
This is debt creation at its most insidious. It’s not a single deliberate loan. It’s not an accidental oversight. It’s a systematic failure to measure the return on investment. Every feature that doesn’t move a metric is carrying a maintenance cost with no offsetting value. Multiply that by twelve features per quarter, and you’ve got a codebase that’s growing heavier without getting stronger.
The features work. The tests pass. But the team is shipping without connecting features to outcomes, which means they have no basis for deciding what to keep, what to kill, and what to invest in further. The debt isn’t in the code quality; it’s in the decision quality.
The fix is connecting every feature to a measurable outcome before it’s built. Not after. Before. If you can’t articulate how you’ll know whether a feature worked, you don’t know enough to build it. That’s not just a product discipline; it’s a debt management strategy. It stops you from accumulating features you can’t evaluate.
When debt becomes toxic
Debt becomes toxic when it’s been compounding long enough that the repayment cost exceeds the original loan.
Consider test coverage on a fast-shipped system. The week-one build had minimal tests; the developer was moving fast, proving a concept, and the LLM was generating code faster than tests could keep up. Each subsequent rebuild addressed the immediate functional problem but didn’t fully pay back the test coverage that should have been part of the repayment.
At five people, low test coverage is manageable. The developer who built it understands it. At fifteen people, it’s a risk factor. At fifty people, it’s a liability: the kind of thing that shows up in investor conversations and acquisition due diligence as a discount to the company’s value.
Like a financial loan where you make interest payments but never touch the principal, a system can get functionally better while remaining structurally no more reliable. The subscription system works. It handles pausing, skipping, tiers, and substitutions. But 60% of that logic is untested, which means every change is a gamble. And rewriting the test suite for a system that fifty people depend on, with live customers in multiple cities, is orders of magnitude harder than writing the tests during the first rebuild.
The interest has swallowed the principal. That’s when debt stops being a tool and starts being a trap.
Every team I’ve worked with has at least one system like this. A piece of code that everyone knows is fragile, that nobody wants to touch, that new developers are warned about on their first day. “Don’t change the billing module” is a sign that the billing module has toxic debt. The warning is the interest payment: the cognitive overhead of working around a system that can’t be safely modified.
The earlier you catch the compounding, the cheaper the repayment. A fragile subscription system at fifteen people is expensive to fix. At fifty-five people, it’s a project. At five people, it would have been a long weekend.
The LLM accelerator
There’s a dimension of technical debt specific to the LLM era, and it’s worth naming: LLMs make it easier to take on debt faster.
A week-one subscription system that used to take a week now takes an afternoon. The LLM compresses the time between “idea” and “working code” so dramatically that debt accumulates before anyone has time to notice they’re taking it on.
This isn’t the LLM’s fault. It’s a tool. But it changes the calculus. When writing code takes days, you have built-in thinking time. The friction of implementation gives you time to notice assumptions, question designs, and spot problems. When the LLM generates a working implementation in an hour, you skip that thinking time. The code exists before the questions are asked.
The discovery techniques are the antidote. Event Storming and Example Mapping front-load the thinking that used to happen naturally during implementation. They force the team to ask questions before the code exists, not after. In an LLM-accelerated world, this isn’t optional; it’s essential. Without it, you can generate a complete, working, wrong system in a week.
Decision tables are particularly useful here. When you give an LLM a decision table (every input combination, every expected output) it can generate code that handles the full domain correctly. Without the table, it generates code that handles the common cases and guesses at the edge cases. The guesses become debt.
The pattern: use discovery to define what’s correct. Use the LLM to generate the implementation. Use tests to verify the LLM got it correct. The discovery is the debt prevention. The LLM is the productivity tool. The tests are the safety net. Remove any of the three and you’re accumulating debt faster than you can track it.
Paying it back
Taking on debt is the easy part. Paying it back is where teams fail.
The most common failure mode isn’t refusing to repay; it’s perpetually deferring repayment. “We’ll fix it next sprint.” Next sprint has its own priorities. “We’ll fix it next quarter.” Next quarter has its own theme. The debt sits in a backlog item that slowly sinks below the fold, occasionally referenced in retros, never actually addressed.
A discipline that helps: the debt ceiling. At any point in time, the team identifies their top three known debts. If a new debt appears that’s worse than the bottom of the three, it bumps the least urgent one out and enters the list. One of the three is always being actively worked on, allocated time in every sprint, not as a special initiative but as part of the regular cadence.
This is the debt equivalent of always be paying the credit card. Not a heroic one-off effort to pay down all the debt at once (that’s a refactoring project, and refactoring projects get cancelled when the next feature request arrives) but a steady trickle of repayment that keeps the total debt manageable.
The standard pushback: “If we spend twenty percent of every sprint on debt repayment, we’re twenty percent slower on features.”
The honest answer: “You’re twenty percent slower on new features. You’re a hundred percent faster on changing existing ones. Which matters more at your stage?”
For a growing, iterating company that’s still discovering what the product should be, the ability to change existing code is worth more than the ability to add new code. Debt repayment isn’t a cost; it’s an investment in changeability.
The debt audit
A complementary quarterly practice: the debt audit. Once per quarter, spend an hour listing every known piece of technical debt, categorising it (strategic, discovery, accumulated, or unmeasured), and estimating the ongoing cost.
The output is a one-page document. Three columns: the debt, the estimated cost per quarter, and the proposed trigger for repayment. The trigger might be a team-size threshold (“fix this before we hire developer number twenty”), a usage threshold (“this breaks at 5,000 subscribers”), or a time threshold (“if we haven’t fixed this by Q3, it becomes the top priority”).
The audit isn’t a planning session. It’s a visibility exercise. Most of the debts on the list don’t get worked on that quarter. But the act of listing them (making them visible, giving them a cost estimate, setting a trigger) prevents the slow drift from “known debt” to “forgotten debt.” And the triggers mean that when a boundary is crossed, the team already knows what needs attention.
The teams that get the most out of the audit describe it as the only meeting where they’re honest about the state of the code: every other meeting is about what we’re building; the audit is about what we’re standing on.
A debt framework
Not all technical debt is equal. Here’s a way to think about it that I’ve found useful.
Strategic debt is Cunningham’s original model. You know you’re taking a shortcut. You know why. You have a plan, or at least a trigger, for repayment. The manual-process pattern is the clearest example. ADR consequences sections document this kind of debt.
Discovery debt is code built on assumptions that haven’t been tested yet. The week-one subscription system. You don’t know the assumptions are wrong when you write the code. Event Storming, Example Mapping, and JTBD research are the tools that reveal this debt. Once revealed, it becomes strategic debt: you now know what’s wrong and can plan the fix.
Accumulated debt is the gradual drift of a system that’s grown without intentional design. The monolith before DDD. Nobody decided to create it. It emerged from hundreds of small decisions, each reasonable in isolation, collectively creating a system that’s harder to change than it should be. The fix isn’t a single refactoring; it’s a design discipline that prevents further accumulation.
Unmeasured debt is the feature factory’s contribution. Features shipped without outcome measurement. You don’t know what’s valuable and what’s waste. Until you instrument it, every feature is Schrodinger’s debt: it might be an asset and it might be a liability, and you can’t tell which.
The cultural dimension
There’s a fifth category that doesn’t fit neatly into the framework but matters enormously: debt as a cultural signal.
When a team accumulates debt without acknowledging it, something happens to the engineering culture. New developers join, see the messy code, and assume that’s the standard. “This is how things are done here.” They write code at the same quality level. The debt normalises.
When a team tracks debt explicitly (in ADRs, in retros, in sprint conversations) the message is different. “This code is messy and we know it. Here’s why it’s messy. Here’s the plan to fix it.” New developers join and understand that the mess is temporary, not permanent. They write better code because the standard is visible, even if the current code doesn’t meet it.
A worked example: a new hire’s first PR is a clean implementation that follows the bounded-context boundaries, because the ADRs tell them where the boundaries are. Their second PR fixes a piece of accumulated debt they noticed during onboarding. Nobody asked them to; they just saw it, understood from the ADR why it was messy, and cleaned it up.
If the debt had been invisible (no ADRs, no documentation, just code that worked in ways nobody could explain) the new hire would have worked around it like everyone else. The documentation turns a new hire from a debt accumulator into a debt repayer.
This is the cultural argument for debt visibility that’s harder to make in a sprint planning meeting than the productivity argument, but it matters just as much in the long run. Teams that track debt attract developers who care about code quality. Teams that ignore debt normalise it and lose the developers who notice.
The principle
Technical debt is a loan, not a crime. The question isn’t whether you have debt; every team does. The question is whether you know about it, whether you took it on deliberately, and whether you have a plan for repayment.
The discovery practices in the rest of this writing, Event Storming, Example Mapping, DDD, don’t eliminate debt. They make it visible. ADRs document it. The audit cadence assesses it. The feature factory is the cautionary tale of what happens when you stop looking.
Ward Cunningham’s insight was that debt can be a tool. You borrow against your current understanding to ship faster, then repay with better understanding later. The teams that get in trouble aren’t the ones who take on debt; they’re the ones who stop tracking it.
If you’re looking at your own codebase and wincing, start with visibility. Map the debt. Not all of it, just the top three things that slow you down the most. Write them down somewhere the team can see them. Give each one a rough cost estimate, even if it’s a guess. Then ask: which of these would give us the most changeability per hour of investment?
You don’t need a grand refactoring initiative. You don’t need “tech debt sprint.” You need twenty percent of each sprint, consistently, working on the thing that slows you down the most. The debt didn’t accumulate in one sprint. It won’t be repaid in one sprint. But a team that’s steadily paying it down is a team that’s getting faster over time, not slower. And that’s a competitive advantage that compounds just as relentlessly as the debt does.
The financial metaphor holds further than most people take it. Debt used wisely (with clear terms, a repayment plan, and an understanding of the interest rate) builds wealth. Debt used carelessly destroys it. The same is true of code. Strategic debt, taken deliberately and tracked honestly, lets you build something you couldn’t have built otherwise. Accidental debt, ignored and compounding, eventually brings the system down. The difference isn’t whether you borrow. It’s whether you know what you owe.
Related references
- Event Storming, where domain misunderstanding gets caught before it becomes debt
- Example Mapping, red cards as early debt detection
- Domain-Driven Design, drawing boundaries around an accidental monolith
- Architecture Decision Records, documenting known debt with repayment triggers
- Decision Tables, formalising domain logic to prevent accidental debt
- Catching the Wrong Kind of Fast, worked example of a week-one build whose assumptions all turned out to be wrong
- The Greenbox Story, narrative behind the worked examples