Decoupling Level: Measuring How Replaceable Your Modules Are
Every module you could rewrite this weekend without telling anyone is an option your architecture holds for you. In 2016, five researchers turned that idea into a single percentage and measured it on 129 software projects.
A metric built on options, not coupling
Most architecture metrics measure how tangled a system is. Ran Mo, Yuanfang Cai, Rick Kazman, Lu Xiao and Qiong Feng went the other way. Their ICSE 2016 paper is "Decoupling Level: A New Metric for Architectural Maintenance Complexity". The abstract states the reversal outright:
Instead of measuring how coupled an architecture is, we measure how well the software can be decoupled into small and independently replaceable modules.
Mo, Cai, Kazman, Xiao & Feng, ICSE 2016
The intellectual root is Carliss Baldwin and Kim Clark's Design Rules. The book borrowed a concept from finance to describe modularity: a module isolated behind stable interfaces is an option. You can exercise the option, replacing the module with a better version, without touching anything else. Or you can let it sit, and the rest of the system neither knows nor cares. Many small independent modules make a portfolio of cheap options. One large block is a single bet. The implication for maintenance is direct. The paper puts it this way: an independent module "implies that its bugs can be fixed locally, and changing it will not cause any ripple effects."
The authors measured Decoupling Level, DL from here on, on 129 real projects. The set covers 108 open source and 21 industrial systems, each across multiple releases. Their headline result: the larger the DL, the more bugs and changes stay local. Developers can also work more independently of each other. The definition rests on one structure, the design rule hierarchy, so that comes first.
The hierarchy hiding in your imports
Start with the dependency graph you already have: every source file is a
node, every import is an edge. Lay it out as a matrix with the same
files as rows and columns. A mark in row x, column
y means file x depends on file y.
The literature calls this a design structure matrix. On its own it is
just a different picture of the graph.
The design rule hierarchy, DRH, is a clustering of that matrix published by Cai's group before the DL paper. It sorts files into layers by one rule: a file may only depend on files in layers above it. Files that many others depend on float to the top layer. These are the interfaces, base classes and core types that Baldwin and Clark call design rules. Files that nothing depends on sink to the bottom. Within the bottom layer, files group into modules that are mutually independent. No module there depends on any other. From the dependency matrix's perspective, each can change without disturbing its neighbors. That bottom layer is where the options live.
The six files above make a small library. Everything imports
types.js, so it is the design rule and sits alone in layer
1. report.js imports parse.js and
validate.js. That pushes those two into a middle layer. The
bottom layer holds three modules of one file each:
render.js, export.js and
report.js. None of the three imports another. From the
dependency graph's perspective, each can change without disturbing the
other two.
DL is a weighted count over this structure. The bottom layer is scored
generously. Every small module there contributes its share of the
system's files. Files in upper layers are scored by how little they
influence what sits below them. A middle-layer module keeps the fraction
of lower files that do not depend on it. A design rule that
everything downstream touches keeps nothing. For the toy library:
types.js has all five lower files depending on it, so it
contributes zero. parse.js has one dependent among the
three files below it. It keeps two thirds of its 1/6 share, about 0.11.
validate.js scores the same. The three bottom modules
contribute 1/6 each. Summing: 0.5 + 0.11 + 0.11 = 72%.
That is the entire calculation for this system.
The zero for types.js is deliberate, not a defect in the
formula. A design rule is the part of the system you promised to keep
stable, so it is never itself an option; it exists to make the options
underneath it possible. DL credits only what the stable parts set free.
Deep dependency chains lose by the same logic. Rewire the six files
into a chain, each importing the next, and every file except the head
has the rest of the chain hanging off it. DL drops to 1/6, about 17%.
Decoupling level is the share of a system living in small modules that nothing else depends on. Stable interfaces score zero by design. Deep chains score near zero because the graph offers few parts that can be swapped out alone.
Small modules count more
One refinement separates DL from a naive "share of files in the bottom layer" count. It comes with its own empirical justification. The authors analyzed the DRH modules of 41 projects. The average bottom-layer module held just 2.11 files, and 3.27 across all layers. The paper also leans on the classic finding that people comfortably process about five chunks of information at a time, and turns that observation into a heuristic cutoff of five files. A module with five or fewer files contributes its full share. A larger one is discounted by the inverse of its base-5 logarithm. The penalty is gentle but it compounds: under the heuristic, a big module asks a maintainer to hold too much in their head at once.
The paper's own illustration makes the penalty concrete. Take 100 files in a single layer with no dependencies between modules. Split into 25 modules of 4 files, DL is 100%. Merge them into 4 modules of 25 and it falls to 50%. Two modules of 50, 41%. One module of 100 files, 35%. Nothing about the dependencies changed across those four pictures. The system just offers fewer, coarser options.
Computing it on codebases you know
A disclosure before any numbers. The authors' implementation lives in Titan and its commercial successor DV8. There is no open source reference implementation of the DRH clustering or of DL itself. So I wrote my own import-graph approximation from the published papers, in about a hundred lines of JavaScript. It condenses each import cycle into a single module, since cyclic files can never be layered apart. It then assigns each module the lowest layer its dependencies allow, and applies the paper's scoring rules. The test suite reproduces the paper's published examples, including the 100%, 50%, 41%, 35% series above. Still, I make no claim that my numbers match what DV8 would print for these repos. The dependency input differs too. The paper reverse-engineers class-level dependencies with Understand. I use madge import graphs, same as my propagation cost measurements.
The scoring itself is short enough to show whole:
// modules: [{files, layer, dependents}] from the DRH (see repo)
let dl = 0;
for (const m of modules) {
const share = m.files.length / totalFiles;
if (m.layer < lastLayer) {
// upper layers: keep the share of lower files you leave alone
dl += share * (1 - m.dependents.size / filesBelow(m.layer));
} else if (m.files.length <= 5) {
dl += share; // small independent module: full credit
} else {
dl += share * Math.log(5) / Math.log(m.files.length); // 1/log₅(size)
}
}
I ran this approximate import-graph DL over the same eleven codebases I pinned for the propagation cost post, same releases, same source directories. These are examples, not architecture rankings: the result depends on where I draw the system boundary. The harness, raw graphs and results live in kmaxat/decoupling-level-demo:
$ node analyze.mjs
madge eslint (lib)
files=378 modules=378 layers=14 dl=63.91% pc=4.75%
madge webpack (lib)
files=620 modules=209 layers=9 dl=4.06% pc=66.11%
madge vue-runtime-core (packages/runtime-core/src)
files=65 modules=4 layers=2 dl=37.20% pc=95.46%
...
| Codebase | Release | Files | Decoupling level | Propagation cost |
|---|---|---|---|---|
| moment | 2.30.1 | 446 | 75.72% | 20.59% |
| chalk | v5.4.1 | 10 | 70.00% | 19.00% |
| eslint | v9.0.0 | 378 | 63.91% | 4.75% |
| axios | v1.10.0 | 61 | 62.23% | 13.33% |
| fastify | v5.4.0 | 28 | 61.17% | 17.09% |
| redux | v5.0.1 | 17 | 48.96% | 22.84% |
| vue runtime-core | v3.5.13 | 65 | 37.20% | 95.46% |
| express | v5.1.0 | 6 | 33.33% | 38.89% |
| mmi-demo broken/ | main@32816c7 | 8 | 12.50% | 57.81% |
| mmi-demo fixed/ | main@32816c7 | 8 | 12.50% | 46.88% |
| webpack | v5.99.9 | 620 | 4.06% | 66.11% |
Moment sits on top at 75.72%, despite its middling propagation cost.
Its graph condenses to 428 modules from 446 files, leaving many small
components that this approximation treats as options. Eslint also lands
high at 63.91% for the same structural reason it had the lowest
propagation cost in the set: nearly every rule in
lib/rules/ is a file nothing else imports. In DRH terms
those are hundreds of one-file modules in the bottom layer. Each appears
as a cheap option in this graph. That matches how people actually work on
eslint: you can rewrite a rule without reading the linter's core.
Webpack is the opposite story. Its large strongly connected core
condenses the 620 files to 209 modules, and the logarithmic penalty does
the rest. It lands at 4.06%.
Vue's runtime-core scores 37.20%. The same boundary caveat from the
propagation cost post applies: this is one deliberately cohesive package
inside a monorepo. DL, like PC, has no way to know I handed it a module
rather than a system.
The pair I built for exactly this purpose produced the most useful
negative result. The
mmi-demo library from my
Modularity Maturity Index post was written
twice with the same eight files, once tangled and once clean. Breaking
the cycles changes five graph modules into eight and reduces propagation
cost from 57.81% to 46.88%, yet both versions score 12.50% DL. Under this
approximation, one bottom-layer file supplies all credited DL in both
graphs. The result falsifies my expected ordering and exposes how strongly
the simplified layer construction can dominate the cycle improvement.
Two questions, one matrix
Propagation cost and decoupling level come from the same dependency matrix. On these eleven repos they broadly agree: high PC pairs with low DL. So why keep both? They answer different questions, ripple versus replaceability. And the paper documents cases where the answers split.
The paper's cleanest split comes from three student submissions of the
same ten-week course project, a questionnaire system built at Drexel.
All three reverse-engineer into DSMs of similar size. Submission 1 used
an abstract Question class as a proper design rule, plus a
correctly applied bridge pattern. It scores DL 82% with PC 25%.
Submission 2 also decoupled questions from answers, but botched the
bridge and let its Ranking class extend
Matching. It lands at DL 78%, PC 37%. Submission 3 routed
everything through a Form class that depends on every
question and answer type: DL 18%, PC 51%. PC sees submissions 1 and 2 as
quite different, 25 against 37, though their modular structure is nearly
the same. DL scores them four points apart. It reserves its collapse for
the design that actually destroyed the options.
The stronger evidence is longitudinal. The authors tracked a commercial project, Comm_1 in the paper, through 29 snapshots over six years. Then they asked its architects what actually happened. DL flagged four turning points. A prototype-to-product refactoring took it from 45% to 74%. Three years of deadline-driven feature work wore it from 78% down to 68%. A refactoring in version 2.12 was supposed to help but dropped it from 65% to 48%. A later cleanup brought it back to 64%. The architects confirmed every one, including the embarrassing third. Five new interfaces had been introduced to decouple the system. One of them ended up influencing 133 files, and the big dependency cycles never got broken. An interface layer that fails to decouple is exactly the failure mode propagation cost cannot see. Indeed, PC moved on only one of the four transitions. In the other direction, when hundreds of well-isolated files arrived in one release, PC signaled degradation where the architects saw none. DL held steady.
There is also a plain statistical argument for DL when you can only monitor one number. Across consecutive non-refactoring releases of 16 projects, DL's average coefficient of variation was 2%, against 12% for PC. OpenJPA nearly doubled its file count across nine snapshots, 2,296 files to 4,406. Its DL moved from 67% to 71%. PC swung with a CV of 26% over the same window; the paper shows PC runs systematically smaller for bigger systems. A metric that stays that quiet when nothing real is changing is a promising candidate for an alarm.
What it can't tell you
First, the disclaimers my own numbers carry. My DRH is an approximation.
The papers describe the clustering at a level that leaves real decisions
open. The sharpest open decision is how files group into modules within
a layer, which directly moves DL. My implementation treats each
condensed dependency cluster as its own module, which is defensible but
not certified. Import graphs miss dynamic require,
dependency injection and event wiring. So the absolute values above are
not comparable to numbers from DV8 or from the paper, only to each
other.
Second, size and layer placement interact. The bottom-layer size penalty is logarithmic and barely bites below 25 files, which helps Chalk. But the upper-layer discount can still drive a small graph down to 12.50%, as both MMI variants show. The paper's benchmark distribution comes from real projects, mostly far larger than mine: across 129 projects, average DL was 59%, with fewer than 20% below 46% or above 75%. Moment lands just above that upper threshold and four measured boundaries fall below the lower one. Those placements are orientation, not percentiles for my results, because the extraction and clustering methods differ.
Last, DL measures structure, not fitness for purpose. Vue's runtime-core sits in the lower half of my chart. It is also one of the most successful pieces of JavaScript ever shipped. Its authors chose cohesion inside a package boundary, and they pay the coordination cost knowingly. The metric counts the independent options a design offers. It says nothing about whether the designers wanted options there in the first place.
Tracking it in CI
The stability numbers are what make DL attractive as a fitness function. But the paper's 2%
coefficient of variation does not establish that my approximation will
be equally quiet on every repository. I would start by recording it
alongside architectural changes, not by failing builds. If it proves
stable and meaningful for a particular codebase, it could eventually
become a ratchet. The whole computation is madge plus a hundred lines of
JavaScript. The harness in
kmaxat/decoupling-level-demo recomputes everything in this
post in a few minutes. Adding your own repo to repos.json
is one line. If you measured your codebase with propagation cost after
the last post, the same dependency graph now gives you a second number
to watch.
Propagation cost says how far a change can travel. Decoupling level estimates how much of the system is structurally isolated enough to swap out.
References
- Ran Mo, Yuanfang Cai, Rick Kazman, Lu Xiao, Qiong Feng, "Decoupling Level: A New Metric for Architectural Maintenance Complexity" — ICSE 2016. All quotes, formulas, benchmark percentiles and the Comm_1 case study above come from it.
- Carliss Baldwin, Kim Clark, Design Rules, Volume 1: The Power of Modularity — MIT Press, 2000. The option theory DL is built on.
- Lu Xiao, Yuanfang Cai, Rick Kazman et al., "Design Rule Spaces: A New Model for Representing and Analyzing Software Architecture" — IEEE TSE 2018. The design rule hierarchy machinery in detail.
- DV8 by ArchDia — the authors' commercial implementation of DRH and DL.
- madge — the import-graph extractor used for every measurement here.
- kmaxat/decoupling-level-demo — the DRH approximation, pinned repo list, raw graphs, and results.