F Fact article

Refactoring Is Not Heroism

An information-theoretic argument that every growing codebase gets messy — and why cleaning up is a rhythm, not a rescue.

Every codebase that grows will get messy. This is not a failure of discipline. It follows from how information works.

Most teams treat this mess as a moral failing. They create “tech debt” tickets that rot in the backlog. They plan “refactoring sprints” as if cleaning were a special event that requires permission. They celebrate the developer who finally tackles the mess as a hero.

This framing is wrong. And information theory has explained why since 1948.

1. Information theory already explains it

A codebase is a compression scheme. The folder structure, the class boundaries, the module interfaces — these are an encoding that minimizes the description length of your system. When the encoding fits the content, you can locate any concept with minimal navigation. Cognitive cost is low.

Shannon showed that the best code is matched to the statistics of the data it was built for. It cannot anticipate data it has never seen. If you could perfectly predict future features, those features would carry zero information — they wouldn’t be new.

Therefore: every new feature that carries real information will, to some degree, violate the existing encoding. The structure was designed for what came before. The new thing does not fit perfectly. Description length grows.

This is not a failure of your architecture. It is information theory at work.

2. The tipping point is calculable

The Minimum Description Length principle (Rissanen, 1978) frames the tipping point:

Refactor when L(code | current) > L(code | new) + C(refactor)

Where L is description length — the cognitive cost of understanding the code under a given organizational scheme — and C is the cost of restructuring.

Consider a concrete example: your application’s state management file. It starts clean — user authentication state, a few actions, a reducer. Then the next sprint adds shopping cart state. Then notification preferences. Then a real-time sync layer. Each addition is small and logical — “it’s all state, it belongs here.” Six months later the file is 1,200 lines and four unrelated domains share a single reducer. Changing the cart logic means scrolling past auth and notification code. A bug in the sync layer sends you searching through all four concerns because the state shapes are interleaved.

Now imagine splitting along domain boundaries: four files, each around 300 lines, each owning its own slice of state. The cost of splitting is real — rewiring imports, updating tests, one afternoon of focused work. But after the split, every future change touches one concern, requires one mental model, and rarely breaks anything outside its boundary.

The formula says: refactor when the ongoing cost of navigating the mess exceeds the one-time cost of cleaning it up. That crossing point is the tipping point.

The tipping point: when restructuring becomes cheaper than continuing.Tipping point: refactor hereKeep buildingRestructuring is cheaperFeatures added since the last restructuringCognitive cost
  • Current structure
  • After restructuring, including its cost C
The tipping point: when restructuring becomes cheaper than continuing.

Software engineering has been measuring proxies for L for decades: cyclomatic complexity (McCabe, 1976), Halstead volume, coupling metrics (Yourdon & Constantine, 1979). And neuroscience has begun to measure the biological side: an fMRI study found that textual size drives programmers’ attention, and vocabulary size taxes working memory — while classic complexity metrics explain only part of what makes code hard (Peitek et al., ICSE 2021, Distinguished Paper Award). Human working memory holds approximately four independent chunks (Cowan, 2001).

We built a composite formula that combines these signals:

S(file) = 0.40 · (LOC / 400) · (avg_cc / baseline_cc)
        + 0.25 · (1 − cohesion)
        + 0.20 · (concerns / 4)
        + 0.15 · (imports / median)

Where:

400
is the empirical window in which code review stays effective, in lines of code
avg_cc / baseline_cc
is the cyclomatic complexity damper — the average cyclomatic complexity per function divided by the codebase median. Simple boilerplate (cc ≈ 1) dampens the size signal; deeply nested logic (cc ≫ baseline) amplifies it
cohesion
is the semantic similarity between code sections (0–1, measured via embeddings)
4
is the working memory chunk limit (Cowan, 2001)
imports / median
normalizes dependency fan-out against the codebase
0.60
0.13
0.25
0.38
= 1.36
Threshold 1.50
  • File size (600 / 400) = 0.60
  • Low cohesion (1 − 0.48) = 0.13
  • Concerns (5 / 4) = 0.25
  • Dependencies (20 / 8) = 0.38

Below the threshold: keep building

Score breakdown for a 600-line file with five concerns and low cohesion.

The weights are our heuristic reading of that research, not values it measured: size weighs most (0.40), followed by how many unrelated things a file does (cohesion: 0.25, concerns: 0.20), and finally coupling to external modules (0.15).

The cyclomatic complexity factor acts as a damper on the size term, not as an independent weight. A 600-line file of identical four-line mappings has a cc near 1 — the damper shrinks its size contribution, and the formula correctly scores it as harmless. A 400-line file with deeply branching conditionals has a cc well above baseline — the damper amplifies the size signal, flagging it even though the raw line count looks moderate. Size tells you how much code there is. Cyclomatic complexity tells you how hard each line is to reason about. The damper combines both into a single measure of cognitive weight.

Cohesion is the other load-bearing term. Without it, the formula cannot distinguish a large file that does one thing from a large file that does fifteen. Cohesion and the cc damper serve different purposes: cohesion separates “many concerns” from “one concern,” while cc separates “complex logic” from “simple repetition.”

3. Testing it against reality

We ran this formula against our own production codebase: 947 source files across six microservices.

In a recent cleanup cycle, we split eight service files that had grown too large — each between 810 and 1,431 lines. No planning, no “refactoring sprint.” We felt the friction and cleaned up. Pure structural refactors: no logic changes, just splitting along seams that were already visible in the code. Tests passed unchanged — a good sign these were structural moves, not behavioral ones.

When we scored those eight files retroactively using the formula, they all fell above the tipping point. The formula matched exactly the splits we had already made by intuition.

When we scanned the full codebase, 17 files scored above threshold. Eight were the ones we had just split. Nine were new candidates we hadn’t noticed yet — files where the cost was accumulating but hadn’t hurt enough to trigger action.

We then used the formula prospectively: the next scan flagged 10 files above threshold. We worked through all of them. Eight splits were genuinely valuable — exposing duplicated logic, mixed read/write concerns, and interleaved domains. Two were false positives: boilerplate dispatchers where every method was an identical four-line delegation. The formula scored them high because of import count and concern count, but there was no actual complexity to reduce. Eight useful flags out of ten on a forward-looking scan, without the formula having been tuned for this round.

The false positives had two defenses that both failed. First, missing cohesion data: the embedding pipeline had silently truncated results, so those files scored with a default low-cohesion penalty instead of their actual (high) cohesion. Second, the cc damper: those dispatchers had a cyclomatic complexity near 1 per function — trivial straight-line delegations. With working cohesion data and the cc damper, both files would have scored well below threshold. Cohesion catches “large but uniform.” The cc damper catches “large but simple.” Together they should remove the class of false positives where a file is big but harmless.

A formula with incomplete inputs can give false confidence. It still found nine candidates our gut feeling had missed entirely, but metrics fail silently. You need to validate the pipeline, not just the math.

The score distribution has the shape the cycle suggests: a right-skewed curve with the bulk of files in a healthy range and a thin tail of files accumulating entropy.

After each split, the files average about 250–420 lines.04008001,2001,6001,431297A×51,169292B×41,099375C×3994259D×4877302E×3831416F×2810405G×2842421H×2
  • Before the split, one file
  • After the split, average per file
  • Cognitive window 250–420
  • Observed split threshold, about 800 lines
After each split, the files average about 250–420 lines. ×n: the number of files each one was split into.

After splitting, file sizes converged to about 250–420 lines per file on average — a range in which a file tends to hold a single concern and stays easy to keep in mind. We read it not as a style preference but as the point where description length is lowest: small enough to comprehend, large enough to be self-contained.

4. Try it yourself: FastAPI

To show the formula works beyond our own codebase, we ran it against FastAPI — a well-known, well-maintained Python framework. No tuning, no special configuration. Just:

python entropy.py fastapi/ --cohesion auto

The flag --cohesion auto computes semantic cohesion locally using ollama embeddings — no API keys, no cloud, just your machine. The script splits each file into logical sections, embeds them, and measures how similar the sections are to each other. High similarity suggests one concern. Low similarity suggests mixed concerns.

Results: 24 files analyzed, 7 above the split threshold.

ScoreFileLinesCCCohesion
15.02applications.py4,03611.90.63
10.74routing.py4,3747.60.59
9.36param_functions.py2,32313.30.71
3.05dependencies/utils.py9636.80.53
2.70openapi/utils.py58211.20.53

The top three files are exactly the kind the formula is designed to catch: thousands of lines, high cyclomatic complexity, and moderate-to-low cohesion. applications.py alone is 4,036 lines with 41 distinct sections — it carries the entire application lifecycle, route registration, middleware stack, and OpenAPI generation in a single file. routing.py is even longer at 4,374 lines. These aren’t bad code — FastAPI is a well-engineered project. But entropy accumulates in every codebase, and these files have crossed the tipping point where splitting would reduce cognitive cost.

The formula also correctly ranks the smaller files low. concurrency.py (37 lines, cohesion 1.0) and exception_handlers.py (28 lines, cohesion 1.0) score near zero — small, focused files that need no attention.

You can reproduce this in a few minutes:

curl -O \
  https://raw.githubusercontent.com/HeinrichvH/articles/main/building-with-ai/01-entropy-cycle/entropy.py
git clone https://github.com/fastapi/fastapi
ollama pull nomic-embed-text
python entropy.py fastapi/fastapi --cohesion auto --all

5. AI changes the tempo, not the cycle

There is a nuance specific to AI-assisted development that gets too little attention.

AI can generate code fast. A pair-programming session with an LLM produces more lines per hour than manual coding. Files grow faster. Features land faster. Entropy accumulates faster.

But AI can also read 800 lines without friction. It doesn’t scroll. It doesn’t tire. So the pain signal — the moment you feel that a file is too large — arrives later than it should. The human is still the bottleneck for comprehension, but the AI masks the symptom by handling the navigation invisibly.

Debugging reveals the cost. AI can assist with debugging too — suggesting hypotheses, reading logs, proposing fixes. But you have to steer it. You have to decide which hypothesis to pursue, which part of the state to inspect, where the real boundary of the problem lies. That steering requires understanding the code. The less you comprehend the structure, the worse your questions become, and the more cycles you waste chasing the wrong lead. AI amplifies your understanding — it does not replace it.

This creates a gap: the theoretical tipping point, where restructuring is cheaper, might be at 500 lines, but you don’t feel it until 800 because your AI partner was carrying the cognitive load for you. By the time you notice, you’ve been paying extra cost for 300 lines.

You have to breathe faster because you’re running faster.

The entropy cycle doesn’t change with AI. The physics is the same. But the tempo needs to increase. And you can’t rely on your gut to tell you when, because AI dulls the signal. You need a formula.

6. The threshold is not a line count

This also reveals an important subtlety: the threshold is not a line count. Take the file from the score breakdown and let it grow to 800 lines, the size at which we saw splits begin. With its five concerns, low cohesion and twenty imports, it now scores about 1.6, above the threshold: split it.

Give the same 800 lines one concern, high cohesion and the median eight imports — all methods serving one purpose, all logic flowing in one direction — and it scores about 1.0, well below. That file should not be split: you would only scatter context that belongs together, and then open four files to understand one concept.

This is why cohesion is in the formula, not just the line count. The tipping point is the balance between two costs:

Splitting too late

scrolling, mental overload, holding irrelevant context

Splitting too early

scattered context, jumping between files for one concept

The formula measures both forces. The score rises when a file is large and incoherent. It stays low when a file is large but focused. Size is the strongest signal, but it is not the only one.

7. Why premature abstraction fails

This framework also explains why over-engineering fails. Premature abstraction is an attempt to build an encoding scheme for information you do not yet have. By the MDL principle, an encoding optimized for hypothetical future data is usually worse for actual current data than a simple, direct encoding. You pay the complexity cost now and receive no compression benefit until the predicted data arrives — if it ever does.

The rhythm

Growth creates mess. Cleaning resolves mess. These are not opposing forces. They are two halves of the same cycle. Like breathing.

Breath 4 of 5 · Cleanup: one part in five stays as structure Core 54 Chaos 150

Resist the mess — refuse to ship anything imperfect — and you never grow. You spend your energy on abstraction layers that anticipate problems you don’t have.

Resist the cleaning — treat refactoring as wasteful — and the mess compounds. Concerns bleed across file boundaries, dependencies grow implicit, and eventually every change requires understanding the entire system because no single piece is self-contained.

The entropy cycle is not a philosophy. It is what information theory predicts for systems that do real work.

Entropy is not your enemy. It is proof that your system is alive.

The only question is whether you have a rhythm for managing it.

The formula described in this article is implemented as an open-source Python script, entropy.py, that works on any codebase — Python, JS/TS, Go, Rust, Java, C# and more. The academic references: Shannon (1948), Rissanen (1978), McCabe (1976), Halstead (1977), Cowan (2001), Peitek et al. (2021), Sturtevant (2013, MIT).

Feeling the friction in your systems?

We help teams find their rhythm between growing and cleaning up — feel free to request a meeting with us.

Stay in the loop

Occasional notes on agents, software, and how we build at Aquiilo. No spam, unsubscribe anytime.

By subscribing you agree we may send you our newsletter. You can unsubscribe with one click from any email.

Was this useful?