Architecture Debt with an Actual Interest Rate
Everyone calls bad architecture "debt", but almost nobody can name the interest rate. A 2016 paper worked out how to compute one from version history. I rebuilt an approximation of it and ran it on five JavaScript codebases.
From metaphor to a number
The four earlier posts in this series measured structure at one instant. Does the architecture pass its fitness functions? How mature is its modularity? How far can a change propagate, and how much of the system is replaceable? None of them said anything about money. Technical debt is a financial metaphor, and a loan has two numbers: the principal and the rate. Every static metric estimates principal at best.
Lu Xiao, Yuanfang Cai, Rick Kazman, Ran Mo and Qiong Feng closed that gap in "Identifying and Quantifying Architectural Debt" at ICSE 2016. Their definition is worth reading twice, because every word of it is measurable:
A group of architecturally connected files that incur high maintenance costs over time due to their flawed connections.
Xiao, Cai, Kazman, Mo & Feng, ICSE 2016
The method has three steps. Find the flawed file groups. Add up what each group costs, release after release. Then fit a curve to the cost and read the interest rate off the curve. On seven Apache projects, the groups they found consumed 51 to 85 percent of all maintenance effort. The five most expensive groups per project held 11 to 32 percent of the error-prone files but consumed 27 to 67 percent of the effort. Debt, it turns out, is concentrated.
One matrix, two kinds of coupling
The instrument behind the method is a
DSM
with a twist. Files are rows and columns, as in the propagation
cost post. But each cell now records two different facts. A
structural mark, dp, says the row file imports the
column file. A percentage says how often the pair changed
together. That is the
HCP:
the probability that the column file changes when the row file
does, computed from the commit log.
Structure and history usually agree. A file that imports another tends to change with it, and files that ignore each other tend to stay strangers. The debts live where the two disagree, and the paper names four shapes of disagreement.
An anchor hub imports its members and is imported by them, and the whole knot changes together. This is spaghetti with evidence. An anchor dominant file is an unstable interface: dependents import it, and when it changes, they change. An anchor submissive file is the reverse, and to me the more interesting smell. Its dependents import it, yet history shows the anchor changing after them. The abstraction leaks upward.
The fourth pattern is the one this series could not see until now. A modularity violation is a pair of files that share no import in either direction yet keep changing in the same commits. They share a secret, a copied algorithm or an assumption about ordering. No parser can find that edge, because the edge is not in the code. Propagation cost and decoupling level both scored these files as independent. The commit log knows better. In the paper's benchmark this invisible category was the most expensive of the four.
Rebuilding it on git history alone
The authors' tooling became DV8, a commercial product, and there is no open source reference implementation. Like the decoupling level experiment, my version is an approximation built from the papers, so I want to be precise about what it does. It is about 350 lines of JavaScript with no dependencies:
-
Structure: resolve every relative
requireandimportin the repo's source tree into a file-to-file edge set. -
History: replay
git log, skip commits touching more than 25 source files as refactor noise, and count how often each file pair appears in the same commit. A pair qualifies as coupled at six shared commits and an HCP of at least 0.5. - Cost: a commit whose message matches fix, bug, patch, or an issue-closing phrase is a bug fix. A debt's penalty is the lines added plus deleted by bug fixes on its files.
- Interest: sample ten release tags spaced evenly across the project's life, accumulate each group's penalty at every boundary, and fit linear, logarithmic and exponential curves. If none reaches R² 0.9, the debt is fluctuating and gets a cubic instead.
One filter matters more than it looks. I only report a modularity
violation when both files still exist at the head commit.
Without that rule, every file that was ever split or renamed
becomes a phantom pair: heavy co-change in history, nothing at
head. My first run made this mistake. It reported eslint's old
eslint.js and rule-context.js as the
repo's biggest debt. Both were deleted years ago in the
flat-config rewrite. The debt was real once, and eslint paid it
off the expensive way.
$ node analyze.mjs
webpack: 633 files, 46 debts, debts 50.6% of effort, top5 24.6%
eslint: 393 files, 4 debts, debts 3.5% of effort, top5 3.5%
prettier: 534 files, 13 debts, debts 17.3% of effort, top5 13.9%
rollup: 272 files, 54 debts, debts 81.1% of effort, top5 52.5%
vite: 138 files, 32 debts, debts 66.5% of effort, top5 32.4%
webpack's plugin family, at 17 percent
The clearest single debt in the five repos is a family of ten
webpack plugins. It spans APIPlugin,
DefinePlugin, HotModuleReplacementPlugin,
NodeStuffPlugin and ProvidePlugin, plus
five module-system plugins under dependencies/. I
checked by hand: none of the ten imports any other. Structurally they are ten independent modules,
and every static metric in this series would praise them.
History disagrees. AMDPlugin and
CommonJsPlugin landed in the same commit 19 times,
with an HCP of 0.53. APIPlugin and
HotModuleReplacementPlugin shared 18 commits. Each
plugin re-implements the same pattern over webpack's parser hooks,
so a change to the pattern is a change to all of them. That is a
textbook modularity violation: the coupling lives in a shared
idea, not in an import.
The family cost 4,106 lines of bug-fix churn across ten release checkpoints from 2014 to 2026. The cumulative curve fits an exponential with R² 0.975. The exponent puts the growth near 17 percent per checkpoint. That is compound interest in the plain financial sense. Each period costs about a sixth more than the one before, and half of the family's total cost landed in the last three checkpoints.
The other three panels show the shapes the paper predicts.
vite's css.ts and moduleGraph.ts pay a
steady 500 lines per checkpoint, a plain subscription fee.
vite's packages.ts and resolve.ts fit a
logarithm. They were expensive early and then settled, which is
what a paid-down debt looks like. webpack's CSS runtime pair was
free for six checkpoints and then surged. No simple curve reaches
the fit floor, so it counts as fluctuating. Across my five repos,
I classified every debt with at least 500 lines of penalty. The
split: 55 percent linear, 28 percent fluctuating, 13 percent
logarithmic and 4 percent exponential. The paper's split over
seven Java projects was 53, 25, 14 and 8. I did not expect a
rough approximation to land that close. The matching order is
the single result I trust most.
Few groups, most of the bill
The paper's headline claim is concentration. A handful of flawed groups eats far more than its share of maintenance. That replicates clearly in four of my five repos.
| repo | source files | commits mined | debts | effort in debts | top-5: files → effort |
|---|---|---|---|---|---|
| webpack | 633 | 7,379 | 46 | 50.6% | 5.5% → 24.6% |
| eslint | 393 | 3,979 | 4 | 3.5% | 2.9% → 3.5% |
| prettier | 534 | 4,125 | 13 | 17.3% | 9.9% → 13.9% |
| rollup | 272 | 2,370 | 54 | 81.1% | 30.1% → 52.5% |
| vite | 138 | 3,901 | 32 | 66.5% | 5.3% → 32.4% |
In webpack, the files inside the top five debts are 5.5 percent of the error-prone files. They account for 24.6 percent of all bug-fix lines, a factor of four and a half. In vite the factor is six. rollup is the indebted one. 81 percent of its entire bug-fix effort flows through detected debt groups. Most of that runs through one 45-file cluster in the AST node hierarchy, whose strongest pair co-changed 36 times with an HCP of 0.86.
eslint is the outlier, and the reason is the phantom filter. The flat-config rewrite dissolved its historically expensive groups. The files no longer exist, so the debts no longer count. What remains measures 3.5 percent. I read that as the metric working. A repo that just paid off its architectural debt should score near zero, and eslint paid with a multi-year rewrite.
Where my numbers bend
The honest list, in the order I would challenge my own results.
My bug-fix heuristic reads commit messages, where the paper
followed issue-tracker links. It misses fixes with terse messages
and counts some refactors that mention a bug. My structural graph
is import-only. Dependency injection, dynamic
require and type-only references are invisible to
it, and each one can turn a truly coupled pair into a false
modularity violation. My release checkpoints are ten tags sampled
across each project's life, not the paper's curated stable
releases. Coarse sampling flattens curve shapes. My pattern
rules are heuristic reconstructions of DV8's detectors, without
the design rule hierarchy underneath. And these are five
JavaScript repos measured with one method. The paper studied
seven Java projects with issue trackers. The numbers rhyme, but
they are not the same experiment.
One mistake deserves its own sentence. An early version of this experiment confidently reported debts made of deleted files, and nothing in the math objected. Any metric built on history needs to check that its files still exist.
Which debts to pay first
The title asked for an interest rate, so here is how I would use one. The curve type is a triage rule. Exponential debts get paid first, whatever their current size. webpack's plugin family is four thousand lines so far, and the rate is still climbing. Linear debts are a predictable fee. Pay them down when the fee starts to rival the cost of the refactor. Logarithmic debts are already paying themselves off, so leave them alone. Fluctuating debts get watched, not paid, until the shape settles. And the groups worth checking first are the ones no static tool will ever flag: files that keep meeting in commits without a single import between them.
An architecture debt is a group of files. Its principal is their bug-fix churn, and its interest rate is the fitted slope of that churn across releases. The most expensive coupling never appears in the import graph, but it shows up in every commit.
References
- Lu Xiao, Yuanfang Cai, Rick Kazman, Ran Mo, Qiong Feng, "Identifying and Quantifying Architectural Debt" — ICSE 2016. The definition, the four patterns, the penalty model, the regression types and all Apache numbers quoted above.
- Lu Xiao, "Identifying and Quantifying Architectural Debt" (CREST talk slides) — UCL CREST Open Workshop. Source of the worked Camel example and the interest-rate distribution pie.
- Lu Xiao, Yuanfang Cai, Rick Kazman, Ran Mo, Qiong Feng, "Detecting the Locations and Predicting the Maintenance Costs of Compound Architectural Debts" — IEEE TSE 2021. The follow-up that predicts next-release debt cost.
- Sunny Wong, Yuanfang Cai, Miryung Kim, Michael Dalton, "Detecting Software Modularity Violations" — ICSE 2011. The origin of the modularity violation pattern.
- Lu Xiao, Yuanfang Cai, Rick Kazman, "Design Rule Spaces: A New Form of Architecture Insight" — ICSE 2014. The DSM-plus-history model the debt work builds on.
- DV8 by ArchDia — the authors' commercial implementation.