Flow & measurement
Your duplication metric is watching the wrong thing
Over sixteen years Flask's function-level duplication fell while the share of its modules caught in dependency cycles climbed from almost none to 0.83. A dashboard built on duplication would have reported steady improvement.
· 6 min
If you wanted a single number to tell you whether a codebase was decaying, duplication would be the obvious choice. A rotting codebase, the intuition goes, fills up with copy-paste. Most code-quality dashboards have a duplication figure near the top for exactly that reason.
I measured it over the full history of four long-lived open-source Python libraries, and in the clearest case it pointed the wrong way for sixteen years.
Four libraries, sixteen years
The subjects were Flask, Requests, HTTPie and boltons. Each has more than a decade of commits, and between them the histories hold close to eight thousand changes made by hundreds of contributors under careful review. I chose them because they are well maintained. If structure decays even here, the problem isn't sloppy teams.
The analyser reads each library's source at pinned commits, builds the graph of which modules import which, and reports two architectural signals. The cycle ratio is the share of modules caught in a dependency cycle, where a change to one module can require knowledge that loops back through the others. Coupling is the average number of other modules each module depends on. Separately, it measures function-level duplication: the share of functions whose bodies are near-copies of at least one other. The analyser and the data are in the coherence ratchet repository, so every number below can be re-run.
Flask is the clearest curve. Its cycle ratio sat at or near zero from 2010 to 2012, reached 0.43 by 2017 and 0.55 by 2019, and was 0.83 in mid-2026. Over the same years its function duplication fell, from about 0.43 early in its life to about 0.25.
Two things move the absolute numbers, and both deserve stating where the numbers are quoted. Imports that exist only for the type checker, which a running program never executes, inflate the later readings: remove them and Flask ends at 0.63 rather than 0.83, and most of the 2021 to 2022 step disappears, because it lands on the merge that added type annotations for Flask 2.0. The climb either side of it stays. And the earliest point covers a package of only a few modules, so a single cycle there is a large share.
Requests tells the same story with coupling. Each module depended on just under one other module in early 2012 and on more than four by 2026. Between 2017 and 2023, the stable years with no major rewrite, its coupling held around 3.1, and it still rose again after that.
The two numbers barely relate
Put the four libraries side by side at their latest measurement and the duplication column tells you almost nothing about the cycle column.
boltons is the check on the instrument. It is a deliberately decoupled collection of standalone utilities, and it reads zero cycles, which is what it should read. It also carries the same level of function duplication as Flask, the most tangled library of the four. HTTPie shows the other useful thing: its cycle ratio rose to 0.33, dropped to about 0.05 after a deliberate de-cycling refactor around 2020, and has since crept back to 0.15. Architectural repair happens, and when it does it is a lumpy, one-off event.
That asymmetry is the most likely explanation for the divergence. Cleaning up functions is cheap and continuous: someone notices two near-identical helpers and merges them in an afternoon. Cleaning up architecture means breaking a cycle, splitting a module that does too much, or restoring a boundary, and those are projects that compete with features. So the function-level picture stays tidy while the architecture drifts, and a metric built on the first can't see the second.
Duplication isn't even the harm
The research on clones points the same way. Rahman, Bird and Devanbu examined clones across several large systems and reported that most bugs were not significantly associated with cloned code, and that cloned code could be less defect-prone than the rest (MSR 2010). Juergens and colleagues separated duplicates that stayed consistent from those that drifted apart, and found that around half of the unintentionally inconsistent clone groups contained genuine faults, a far higher rate than duplication in general (ICSE 2009).
The harm is divergence: copies that started out the same and stopped agreeing, so that a fix made to one is silently not made to the others. That is a structural problem, and you find it by reading the dependency structure and comparing the code against an intended pattern, not by counting how much of it repeats.
Why this matters more with AI
Studies of AI coding agents so far describe them as refactoring locally: they tidy and deduplicate within the code they can see, and rarely make the structural change that would pay down a tangle (Huang et al., 2026; Horikawa et al., 2025). Map that onto the two axes and the worry is specific. Agents are well placed to keep the function-level number healthy, which is the number the dashboards show. They are poorly placed to do the expensive architectural repair, which is the work that already happens least.
The direct test isn't possible yet. Codebases written mostly by agents are too young to have aged, and a three-week-old project reads clean on every measure, just as Flask did for its first years. What the human baseline does establish is where to look.
What to watch instead
Watch the dependency structure: the share of modules in cycles, coupling between modules, and imports that cross a boundary the team meant to keep. Read them per change, so a reviewer can see what a pull request did to the structure next to whether it passed.
Read duplication as a delta, not a grade. A new cluster of near-identical functions that arrived in the last commit is a useful fact about that change. A codebase-wide duplication percentage is a poor verdict on its health, and in Flask's case it would have reported steady improvement for a decade and a half.
Four libraries in one language are not a law. They show that the decay is real and measurable where it has been measured, and that the number most teams would have trusted to warn them was looking at a different axis.
Continue the conversation
Occasional emails from Roman about new writing, books, Flow Observatory and community events. Unsubscribe at any time.
