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.

imports the same files, layered types.js parse.js validate.js report.js render.js export.js typavareexrp typesparsevalidaterenderexportreport layer 1 · design rules layer 2 layer 3 · independent modules every mark sits left of the diagonal: files depend only upward

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 same 100 files, four ways to group them redrawn from Figure 2 of the DL paper; one layer, no dependencies between modules 25 modules × 4 files DL = 100% 4 modules × 25 files DL = 50% 2 modules × 50 files DL = 41% 1 module × 100 files DL = 35%

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%
...
Approximate decoupling level, import graphs at pinned releases structural independence visible inside each selected source boundary 0%25%50%75%100% moment · 446 files moment 2.30.1 src/: 75.72% 75.72% chalk · 10 files chalk v5.4.1 source/: 70.00% 70.00% eslint · 378 files eslint v9.0.0 lib/: 63.91% 63.91% axios · 61 files axios v1.10.0 lib/: 62.23% 62.23% fastify · 28 files fastify v5.4.0 lib/: 61.17% 61.17% redux · 17 files redux v5.0.1 src/: 48.96% 48.96% vue runtime-core · 65 files vue-runtime-core v3.5.13: 37.20% 37.20% express · 6 files express v5.1.0 lib/: 33.33% 33.33% mmi-demo broken · 8 files modularity-maturity-demo broken/: 12.50% 12.50% mmi-demo fixed · 8 files modularity-maturity-demo fixed/: 12.50% 12.50% webpack · 620 files webpack v5.99.9 lib/: 4.06% 4.06%
CodebaseReleaseFilesDecoupling levelPropagation cost
moment2.30.144675.72%20.59%
chalkv5.4.11070.00%19.00%
eslintv9.0.037863.91%4.75%
axiosv1.10.06162.23%13.33%
fastifyv5.4.02861.17%17.09%
reduxv5.0.11748.96%22.84%
vue runtime-corev3.5.136537.20%95.46%
expressv5.1.0633.33%38.89%
mmi-demo broken/main@32816c7812.50%57.81%
mmi-demo fixed/main@32816c7812.50%46.88%
webpackv5.99.96204.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.

Ripple against replaceability, eleven codebases 0%25%50%75%100% 0%25%50%75%100% propagation cost → decoupling level → eslint axios fastify chalk moment redux express mmi fixed mmi broken webpack vue runtime-core

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