The Modularity Maturity Index
Every team eventually argues about whether a codebase is worth saving. The argument usually runs on gut feeling. The Modularity Maturity Index is an attempt to run it on numbers instead.
Where the index comes from
The idea is older than the name. In the 1970s, structured design gave us coupling and cohesion: keep related things together, keep unrelated things apart. In the 1990s, Robert C. Martin sharpened one corner of it with the Acyclic Dependencies Principle, the rule that your dependency graph should never loop back on itself.
The Modularity Maturity Index pulls these threads into a single number. It was defined by Carola Lilienthal, a German software architect who spent years reviewing large systems with architecture-analysis tools and kept seeing the same few properties separate the maintainable codebases from the doomed ones. She gathered them into one 0-to-10 score and published it in her book Sustainable Software Architecture (2019, after the 2015 German edition).
The reason it works is not really about computers. Software has to fit inside a human head, and a head holds only a handful of things at once. Modular, layered, repetitive code lets you reason about one chunk at a time and trust the rest. The index measures how kind a codebase is to the person who has to hold it in their mind — the conundrum in the picture above.
The three pillars, drawn out
A high score rests on three properties. They sound alike but they fail in different ways, so it is worth seeing each one as a picture of the broken case next to the healthy one.
1. Modularity
Each building block should do one job and lean on as few others as possible. The broken case is a "junk drawer": one module that does formatting, tax, logging, and validation, which half the codebase then has to import. The fix is to split it so each module has a single reason to exist. Measured by average dependencies per module and the single most depended-on module.
2. Hierarchy
Dependencies should flow one way, like water downhill. The broken
case is a cycle: cart needs pricing,
pricing needs discount,
discount needs cart — so none of
them has a meaning on its own and none can be tested alone. The fix
is to arrange them in layers that only ever point downward.
Measured by counting the cycles in the dependency graph.
3. Pattern consistency
The same kind of problem should be solved the same way every time.
The broken case signals a bad item three different ways —
throw a string, return null, return
{ok:false} — so every caller has to remember
which module does which. The fix is to pick one convention and hold
it everywhere. This is the pillar a tool cannot grade for you; it
takes a human eye, which is worth being honest about.
The three roll up into one decision:
flowchart LR A[Pick a source tree] --> B["Build the module
dependency graph"] B --> C["Score modularity,
hierarchy, patterns"] C --> D{0 to 10} D -->|above 8| E[Keep] D -->|4 to 8| F[Refactor] D -->|below 4| G[Rewrite or rethink]
I broke a library on purpose
Reading about pillars is one thing; feeling them is another. So I
wrote a tiny pricing library twice and put both versions in one
public repo,
kmaxat/modularity-maturity-demo.
The broken/ folder violates all three pillars. The
fixed/ folder repairs them. Same eight modules, same
feature — turn a list of items into a receipt — and
byte-for-byte the same output. Only the structure changes.
Point madge at the broken version and the rot shows up:
$ npx madge --circular broken/src
Processed 8 files
1) cart.js > pricing.js
2) pricing.js > discount.js
3) utils.js > format.js
The fix is the three pictures above, applied in order:
-
Modularity. The
utils.jsjunk drawer — imported by six of the eight modules — is split intomoney,tax, and friends, each with one job. -
Hierarchy. The cycles are cut by giving the code
layers:
receipt → cart → pricing → {money, tax, discount}, every arrow pointing down. -
Pattern consistency. Three error styles become
one thrown
InvalidItemError, and three money formats (floats, cents, strings) become integer cents everywhere.
Run the same command on the fixed version:
$ npx madge --circular fixed/src
Processed 8 files
✓ no circular dependencies
Same features, very different shape:
| Modules | Deps / module | Busiest module | Cycles | |
|---|---|---|---|---|
| broken | 8 | 2.00 | utils.js (imported by 6) | 3 |
| fixed | 8 | 1.25 | money.js (imported by 3) | 0 |
Note that money.js in the fixed version is still
imported a lot. That is fine. It is small, does one thing, and
imports nothing back — a stable foundation, not a junk drawer.
High traffic into a cohesive module is healthy; high traffic into a
grab-bag is the problem.
The same yardstick on real code
The toy is rigged. To check the idea against code I did not write, I ran the same measurement over six well-known projects:
| Project (source) | Modules | Deps / module | Busiest module (imported by) | Cycles | Files in cycles |
|---|---|---|---|---|---|
| express | 6 | 1.0 | utils (2) | 0 | 0% |
| axios | 65 | 2.4 | utils (26) | 0 | 0% |
| eslint | 392 | 1.7 | ast-utils (191) | 0 | 0% |
| webpack | 677 | 3.8 | RuntimeGlobals (136) | 309 | 45% |
| vue (runtime-core) | 124 | 6.5 | shared (83) | 232 | 69% |
| TypeScript (compiler) | 77 | 2.1 | ts namespace (73) | 74 | 99% |
- Size is not destiny. eslint has 392 modules and not one cycle. The TypeScript compiler has 77 files and 99% of them sit in a cycle. Discipline decides hierarchy, not line count.
-
The pillars genuinely disagree. eslint's
hierarchy is spotless, yet its
ast-utilsmodule is imported by 191 of its 392 files. Clean on one axis, heavy on another — which is exactly why you keep the pillars separate instead of blending them into one number. - Coupling climbs with ambition. express averages one dependency per module; vue's reactive runtime averages six and a half. More moving parts mean more wiring — some essential, some not.
What the number is not
The lazy reading is that express is mature and the TypeScript compiler is a mess. That reading is wrong, and the reason it is wrong is the whole point. The compiler is some of the most careful code in the ecosystem; its cycles come partly from a deliberate namespace pattern and from the trade-offs a performance-critical monolith makes. A low score is not a guilty verdict. It is a map of where the friction lives.
That is why the index is a guide, not a grade. My demo library scored badly on every pillar while producing the exact same receipt as the clean one — proof that the number says nothing about what the code does, only about how hard it is to hold in your head.
What the index really buys you is the thing the two stick figures were missing: a way to turn "this feels tangled" into three cycles and a junk-drawer module you can point at.
References
- Carola Lilienthal, Sustainable Software Architecture: Analyze and Reduce Technical Debt (dpunkt.verlag, 2019) — the source of the Modularity Maturity Index.
- Robert C. Martin, the Acyclic Dependencies Principle — on keeping the dependency graph free of cycles.
- Coupling and cohesion — the structured-design ideas the index builds on.
- George A. Miller, "The Magical Number Seven, Plus or Minus Two" (1956) — on the limits of working memory.
- madge — the dependency-graph tool used for every measurement here.
- kmaxat/modularity-maturity-demo — the broken-then-fixed demo library.