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.

It's fine —just refactor it. It's hopeless —rewrite it. the codebase Two engineers. One codebase. Zero data.

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.

one junk drawer, low cohesion one job per module, high cohesion utils.js format()calcTax()log()validate() money()tax()log()validate()

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.

a cycle: nothing stands alone layers: dependencies point down cartpricingdiscount receiptcartmoneytax

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.

same job, three different ways same job, one way throw "bad item"return nullreturn {ok:false} throw InvalidItemErrorthrow InvalidItemErrorthrow InvalidItemError

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.js junk drawer — imported by six of the eight modules — is split into money, 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
broken82.00utils.js (imported by 6)3
fixed81.25money.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
express61.0utils (2)00%
axios652.4utils (26)00%
eslint3921.7ast-utils (191)00%
webpack6773.8RuntimeGlobals (136)30945%
vue (runtime-core)1246.5shared (83)23269%
TypeScript (compiler)772.1ts namespace (73)7499%
  • 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-utils module 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