You just fixed a bug in the payment module. Simple enough — an edge case in the calculation logic. You changed three files: the payment handler, a date formatting utility, and a config loader.
Those three files have nothing to do with each other.
You know that. You've always known it. You fixed the bug anyway, committed all three, and moved on. Because that's just how this codebase works.
Your import graph is lying to you
When we talk about coupling, most developers think of import statements. You open a file, you look at what it imports, and you draw a mental map of dependencies. Clean Architecture tools do the same thing — they trace the explicit links between modules and draw you a diagram.
The problem is that explicit links are only half the picture.
What no import graph captures is this: the fact that every time you touch the payment handler, you also have to touch the date formatter. Not because one imports the other. Because some shared assumption lives between them — a format agreed upon in passing, a constant defined in neither place but expected in both, a behavior that emerged from the way they were built together and was never written down.
These are behavioral dependencies. They don't show up in your IDE. They show up in your commits.
What git history actually records
Every commit is a unit of change. But it's also something else: a record of what the system forced you to change together.
When you fixed that bug and touched three files, the system was telling you something. It was telling you that those three files are coupled — not in code, but in behavior. They share an assumption. They carry a hidden contract. And git has been recording that fact every time it happened, for months, for years, in every commit that touched more than one thing it probably shouldn't have.
This is what software forensics researchers call change coupling: two files that consistently appear in the same commit, across many commits, over time. Not once — because co-editing happens. Consistently. Across your team, across your sprints, across your entire project history.
A pair of files that co-commits 25 times in six months is not a coincidence. It's a relationship. It just hasn't been named yet.
The pattern you already know
Think back to the scenario from part one — the field that took twenty minutes but three days to deliver.
"The field touched a model. That model was wired straight into a service. That service was called from four places you didn't know existed."
That cascade didn't come from nowhere. It had been building in the git history for months. Every time someone touched the model, they also touched the service. Every time someone touched the service, two of those four callers moved too. The pattern was there. Nobody read it.
Your codebase leaves the same traces. The coupling that will cost your next developer three days is already visible — if you know what you're looking at.
How to read it yourself
You don't need a tool to start. Open your terminal and run:
git log --pretty=format: --name-only | sort | uniq -c | sort -rn | head -20
That gives you the files changed most often. Now take your top five and look at which files travel with them. Pick any high-churn file and run:
git log --all --name-only --follow -- path/to/your/file.py
Scroll through the commits. Notice which other files keep showing up alongside it. You're not looking for files that appear once — you're looking for files that appear together consistently, in commit after commit, for no obvious reason.
Those pairs are your hidden couplings. And here is what makes them dangerous: they're invisible to anyone who joins the team next month. No onboarding document lists them. No architecture diagram shows them. The only place they're recorded is in the history — and only if someone thinks to look.
If you want to go further than a one-off snapshot, Calyntro continuously tracks these patterns across your history. Instead of a list of files, you get coupling trends over time — so you can see whether a pair is getting worse, stabilizing, or quietly resolving itself as the codebase evolves.
What the pattern reveals
Change coupling tells you three things that no other tool can:
Where your actual dependencies are — not the ones in your import statements, but the ones enforced by behavior and shared assumptions. These are the dependencies that will surprise you when you try to change something "simple."
Where your estimates will be wrong — any ticket that touches a file with strong change coupling will take longer than expected, because the change won't stay contained. It will pull the coupled files with it, every time.
Where to start decoupling — the strongest coupling pairs are your highest-leverage refactoring targets. Not because they're complex (that's part three), but because breaking the hidden dependency makes every future change in that area cheaper.
Reading the signal before it costs you
The field that took three days had a git history. The coupling that made it slow had been accumulating for months before that ticket arrived. The signals were there — files co-committing that had no business being in the same commit, a growing list of "surprise" file additions every time someone touched a certain module.
Your codebase is leaving the same signals right now. The question is whether you read them before the next three-day ticket, or after.
Take twenty minutes this week. Find a file you've changed more than three times in the last month. Look at what traveled with it. What you find might be the most useful thing you learn about your architecture this year.
Part of the Craft of Evolutionary Architecture series. Part 1 looked at what tight coupling actually costs — not in theory, but in your week. Part 3 goes deeper: why the most-changed files are usually also the most complex — and what that combination tells you about where to start.
Top comments (0)