Software Architecture · Technical Debt
Principal, Interest, and Upkeep in the Technical Debt Model
What the debt metaphor actually quantifies: a one-time restoration cost, a recurring surcharge on future changes, and the arithmetic that decides whether repayment ever pays.
A design that is not quite right still ships. The team that ships it will spend extra effort on every later change that touches the same ground, and the metaphor of technical debt exists to make that extra effort visible before it is spent. Ward Cunningham introduced the phrase in a 1992 experience report on a portfolio management system, and the framing there was deliberately narrow: the team had an immature domain model, chose to put it into users’ hands anyway, and accepted that consolidating the model later would cost more than building it correctly the first time. The consolidation cost was the principal. The extra work caused in the meantime was the interest.
That framing is worth recovering because the phrase has drifted. It is used now for everything from a missing index to a codebase that nobody understands, and in the drift the quantitative content disappears. The quantitative content is the whole point. A debt is only a debt if it can be described as a sum that could be paid once in exchange for a reduction in a recurring charge.
Two quantities and one comparison
Every useful debt statement has the same three parts. The principal is the one-time cost of restoring the property that is missing: extracting a module, replacing a hand-rolled queue with one whose ordering guarantee is documented, moving a rule that exists in four places into one. The interest is the additional cost per unit of future work attributable to the absence of that property, measured in whatever unit the work is priced in — hours, review cycles, defect escapes. The horizon is how many of those units are expected before the code stops changing.
The comparison is arithmetic. If interest is $i$ per change and the principal is $P$, repayment breaks even after $P / i$ changes. Below that many changes, the shortcut was the cheaper choice; above it, the shortcut was a loss. In a codebase where a module is edited a few times a year and the interest is a slightly slower edit, the break-even point can lie beyond the lifetime of the product, and paying the principal is a pure loss. In a module edited every week, the same shortcut pays for itself within about half a year.
This is why the question “is this code bad?” has no answer and “is this debt worth repaying?” does. The same file can be debt in one system and a reasonable permanent choice in another, and the difference is the expected number of future edits, not the aesthetics of the code.
def payback_changes(principal_hours, interest_hours_per_change):
"""Number of changes before repayment breaks even."""
if interest_hours_per_change <= 0:
return float("inf") # never comes due under this model
return principal_hours / interest_hours_per_change
# A module with two edits a year and 40 hours of principal:
changes_per_year = 2
print(payback_changes(40, 1.5) / changes_per_year) # ~13 years: not worth it
print(payback_changes(40, 1.5) / 20) # ~1.3 years: worth it
The arithmetic is not a measurement. It is a way of exposing which assumption a repayment decision rests on: the interest rate and the horizon. Disagreements about whether to refactor are almost always disagreements about one of those two numbers, stated as aesthetic judgments because the numbers were never named.
Why the charge compounds instead of accumulating
Interest is often described as compounding, and the mechanism is worth being precise about, because it is not interest-on-interest in the financial sense. It is replication.
A shortcut is a template. The next similar feature is written by reading the existing one, so the wrong shape is copied; tests are then written that encode the copied behaviour as correct; documentation and dashboards are built against the shape; interfaces are exposed that assume it. Each of these adds a dependent, and the cost of removing the shortcut becomes the cost of unwinding every dependent. The principal grows with the number of copies, not with the passage of time. A shortcut left in a module that nobody extends has a stable principal and near-zero interest. A shortcut that becomes the house style has a principal that rises every month whether or not anyone touches the original.
This gives a concrete diagnostic that does not require measurement infrastructure: count the dependents. A design flaw with no dependents is cheap to fix whenever it is fixed. A design flaw with a hundred call sites is expensive, and the expense is a function of the call sites, not of how long the flaw has existed.
It also explains why the discipline that matters most is not continuous refactoring but refusing to copy. The moment a shortcut is used as a model for new code, the loan is refinanced at a worse rate.
Three currencies that interest is paid in
The recurring charge is usually described as “slower changes”, which hides three distinct costs with different mechanisms and different remedies.
Comprehension. A reader must hold more of the system in mind to predict the effect of an edit. In a module with a redundant state variable, the reader has to work out which of the two sources of truth is authoritative at each point, and the answer often depends on call order. The cost scales with the number of independent facts that must be simultaneously held; human working memory holds roughly a handful of items, so a design that requires eleven is not merely slower, it is error-prone in a way that no amount of care fixes.
Coordination. If a property is not enforced by a boundary, it must be enforced by agreement. A rule implemented in four services is a rule that four teams must change together; the cost appears as scheduling latency, not as coding time, and it grows with the number of owners. A rule behind one interface owned by one team converts a coordination problem into a code change.
Verification. A shortcut often widens the set of reachable states, and testing cost grows with that set. A boolean flag that selects between two behaviours looks free, but $n$ independent flags produce $2^n$ configurations, and each one is a state that can ship broken. Flags left in place after their rollout completes are the canonical example: the code is correct in the configuration that is tested, and the other configurations are untested code that still runs in someone’s environment.
The remedies differ accordingly. Comprehension debt is repaid by renaming, splitting, and deleting; coordination debt by moving a decision behind an interface; verification debt by removing configuration. Treating all three as “clean up the code” produces refactoring that fixes the first and leaves the other two.
Debt that never comes due
The definition implies something uncomfortable: code with no expected future change carries no interest, so its principal is sunk. A dead module that is ugly, undocumented, and wrong is not technical debt under this model. It is a liability of a different kind — it occupies space in the repository, appears in search results, and must be compiled or scanned — but nobody will pay interest on it because nobody will change it.
Two consequences follow. First, deletion is often a better instrument than refactoring: removing code eliminates both principal and interest at once, and it is usually cheaper than improving code that no longer earns its keep. Second, ugly-but-stable code deserves to be left alone. The instinct to improve everything that looks wrong converts a zero-interest liability into a project with a real cost and no return, and it consumes the capacity that the genuinely hot modules need.
The honest way to price interest is therefore to look at where changes actually land. Version history is the only record of expected future change that is not a guess, and a file’s edit frequency is a better predictor of its interest than any static property of the code. This is the reasoning behind churn-weighted analysis, discussed in Measuring Debt Without Inventing Numbers.
Where the arithmetic stops working
The two-number model breaks in several specific places, and knowing them prevents the model from becoming a justification for whatever the team already wanted to do.
Interest is not paid in a single currency. Time saved per change, defect probability, onboarding duration, and the willingness of engineers to work in a module are not commensurable, and any exchange rate between them is a judgment. A model that prices everything in hours will systematically understate the cost of a module that is correct but hostile to read, because the hostility shows up as turnover and avoidance rather than as a line item.
Principal estimates exclude the parts that dominate. The code change is usually the small part; migration of stored data, dual-running of old and new paths, retraining, and the temporary reduction in delivery capacity during the transition are the large parts. Estimates that count only the code change understate principal by a factor that depends on how many dependents exist, which is exactly the quantity the decision turns on.
Debt can be refinanced into worse debt. Replacing a framework with a newer one can pay down one constraint while taking on a new set of conventions, a new release cadence, and a new set of unknowns. The comparison is not old-versus-new but total-interest-before versus total-interest-after, including the interest on whatever the new choice defers.
Some “debt” is not debt. An abstraction that was never built because the second use case never arrived is not a loan taken out; it is a cost avoided. The distinction matters because teams that treat every absent abstraction as debt build speculative structure, and speculative structure carries its own interest: every unused extension point must be understood and maintained. Martin Fowler’s quadrant separates deliberate from inadvertent debt and prudent from reckless debt, and the useful cell is deliberate-and-prudent, which requires the debt to be recorded with the condition under which it will be repaid.
What a debt entry has to contain
A record that supports the arithmetic has four fields, and any of them missing turns the entry into an opinion: the property that is missing, the interest it charges and per what unit of work, the principal to restore it, and the event that triggers repayment. The trigger is the field teams omit, and its absence is what makes debt permanent. “Revisit when the ingestion path handles a second event type” is a trigger. “Clean this up eventually” is not.
Once entries exist in that form, the question stops being whether the codebase is good and becomes an ordering problem: given limited capacity, which principals should be paid first, and which should be allowed to run to term. That question is taken up in Sequencing Repayment Under a Fixed Capacity.
The model’s limits are worth stating plainly at the end. It prices the cost of change, not correctness: a system can have low interest and still be wrong. It assumes future change, so it says nothing useful about frozen code. And its two numbers are estimates whose uncertainty is often larger than the gap between the options. What it does provide is a shared vocabulary for the two facts that decide the question — how often this ground is touched, and what it would cost to stop paying.
References
- The WyCash Portfolio Management System — Ward Cunningham, OOPSLA 1992 experience report
- TechnicalDebtQuadrant — Martin Fowler
- Technical Debt: From Metaphor to Theory and Practice — Kruchten, Nord, and Ozkaya, IEEE Software, 2012
- Programs, Life Cycles, and Laws of Software Evolution — M. M. Lehman, Proceedings of the IEEE, 1980