What is Modularity?
"95% of the words are spent extolling the benefits of "modularity" and little, if anything, is said about how to achieve it" - Glenford Myers
Having Modules Is Not Modularity
Modularity is one of those things in Software Engineering that everyone promotes but nobody defines; if you asked 400 developers what modularity is, you'd probably get 400 different definitions. Glenford Myers noticed this in 1978, and it his observation aged like fine wine:
The trouble is that "modular" sounds like a thing you can check by looking. Are there folders? Are there packages? Are there services? Yes? Then it is modular, congratulations!
But directories are free. Anyone can create a folder. What you are actually after is a property of the system, not of its file layout, and the most-straightforward way I have found to define it is as follows:
A system is modular to the degree that you can predict what a change will break.
Vlad Khononov's article explains it well: "Modular design ensures that the outcomes of actions and changes are clear"
In terms of features/requirements to be implemented in a software solution; this can be broken down in 2 points:
- When a requirement lands, it is clear which part has to change.
- Once you change a feature, it is clear what things are affected, and the answer is(or atleast should be) "small".
If you made a change in your code and can't confidently say what else changed, or if a requirement arrives and three devs have to argue about where it goes you probably don't have modularity.
I like this definition because it is falsifiable. You can test it against last week.
Splitting a system does not reduce its complexity. It relocates it.
Some complexity is just part of the problem you're solving. You can't always refactor it away because it's not coming from the code, it's coming from the problem itself.
When you split things into multiple modules, that complexity doesn't disappear. You move it from inside one module to the interactions between those modules, which can be harder to see and reason about.
A concrete example.
Let's say you have a consent management module with a method that grants consent:
public Consent grantConsent(String subjectId, String scope) {
if (!scopeValidator.isValid(scope)) {
throw new InvalidScopeException(scope);
}
Consent consent = consentStore.insert(subjectId, scope, Instant.now());
// must happen after the insert commits
eventPublisher.publish(new ConsentGranted(consent.id(), subjectId, scope));
return consent;
}Nothing is really wrong with this method. Each step feeds the next, so they belong together.
But later, consent also needs to be granted by a bulk import and an admin console. All three need to store the consent and publish the event, so you extract the pieces for reuse.
That's reasonable.
Then the import job does this:
public void importConsents(List<ConsentRow> rows) {
for (ConsentRow row : rows) {
eventPublisher.publish(new ConsentGranted(row.id(), row.subjectId(), row.scope()));
consentStore.insert(row.subjectId(), row.scope(), Instant.now());
}
}It looks reasonable. It compiles. Both collaborators are called.
But the ordering is now wrong. A downstream consumer can receive an event for a consent that doesn't exist yet.
Before the split, the ordering rule was enforced by the structure of the method. The two statements were next to each other, in the required order, with the reason documented above them.
After the split, that rule lives nowhere:
ConsentStoredoesn't know it has to run first.EventPublisherdoesn't know it has to run after the commit.- Neither interface can express the ordering.
- Neither class's unit tests can catch the violation because both are individually correct.
The bug isn't really in any one of them.
It's in the interaction between them.
That's the complexity that moved.
The solid boxes are what your tools can see. The dotted arrows are the rules between them.
Someone might say: just put a facade over the three pieces and keep the ordering there.
That's probably the right fix.
But notice what happened: we created a class whose job is to hold the sequencing knowledge that the split displaced.
The complexity didn't disappear. We just gave it a new home.
This is why "just keep coupling low" isn't enough as architectural advice.
You can keep splitting things until individual modules look beautifully simple, while the relationships between them become harder to understand.
Modularity isn't something you maximise.
It's a trade-off.
The goal isn't the smallest modules or the fewest dependencies. It's putting complexity somewhere that makes the system easier to understand, change, and test.
Three Lenses
Let's go back to that import bug scenario for a second. Supposing that this was a real world scenario; this could have very easily reached production, and caused problems, regardless of the tests put in place. That's the uncomfortable part of finding the balance point for modularity. You can't review your way to it, because the thing you're judging isn't necessarily inside the code itself. So we need instruments to help us with this.
Fortunately, according to Mark Richards, we can make use of 3 of them. Each one making a different part of our problem visible.
Cohesion looks inward
Do the things inside this module actually belong together?
It looks at one module by itself. Basically, before we even worry about where the boundary should be, do these things actually make sense as one thing?
This is also the most subjective of the three.
Computer scientists have been defining different levels of cohesion for decades, from functional at the top to coincidental at the bottom. There is even LCOM, which tries to measure cohesion structurally.
But in practice, a lot of this still comes down to judgment.
The genuinely hard case is when the honest answer is: "It depends on whether this grows."
And nobody knows whether it will.
That case has a good answer, and it's the whole of the next post.
Coupling looks outward and counts
How many things depend on this module, and how many things does it depend on?
That's the basic question.
Incoming connections are afferent coupling. Outgoing connections are efferent coupling. From those you can derive things like Instability, Abstractness, and Distance from the Main Sequence.
Some of these are useful. Most aren't worth obsessing over.
The durable idea is simpler:
The more things depend on you, the more careful you need to be about what you expose.
Something that forty modules depend on and that is entirely concrete isn't necessarily stable. It might just be stuck.
I'll get into which coupling metrics are actually useful in post three.
Connascence asks what a change costs
And this is where the first two leave a gap.
Coupling counts dependencies. It doesn't tell you how painful those dependencies are.
Two modules can have exactly one connection between them, which looks great from a coupling perspective, and still be tightly coupled because that one connection represents a shared algorithm that has to change on both sides.
Another pair might have six connections and still be easy to separate because all six are just method calls that a rename refactor can fix in an afternoon.
Connascence gives you a vocabulary for that difference.
Two components are connascent when changing one requires you to change the other to keep the system correct.
And it comes in different strengths, from agreeing on a name at the cheap end to having to keep values consistent across two different databases at the expensive end.
It's probably the most useful of the three lenses and also the least familiar, which is why it gets the longest post.
What These Lenses Will Not Do
Three honest limits, because a post with only upside is marketing.
They see structure, not intent.
LCOM can tell you that two parts of a class touch different fields. It can't tell you that both belong together because the business rules require them to change together.
That's why metrics can tell you how something looks, but not necessarily why it is that way.
Two of the three have very limited tooling.
Cohesion is mostly judgment with some metrics around it. Connascence doesn't have the kind of standard tooling that coupling or complexity metrics have.
And there's a real failure mode here: a team learns the vocabulary, starts saying things like "that's Connascence of Meaning" in code review, and changes absolutely nothing about the code.
Vocabulary feels like progress.
It isn't progress.
Metrics cannot separate essential complexity from accidental complexity.
Cyclomatic Complexity can tell you that a function is complex. It can't tell you whether the underlying problem is complex or whether the code is simply bad.
Every number still needs a human to interpret it.
That's why I don't think these metrics should automatically become build gates. First you need to understand what the number means in your system and what a reasonable baseline looks like.
None of that makes them useless.
It just means they're instruments, not answers.
And instruments are still a huge upgrade over arguing about folder structure.
Key Takeaways
- Modularity is about predictability, not structure. You have it when a requirement lands in an obvious place and the blast radius of changing it is small and understandable. Directories are free.
- Granularity is where the damage happens. Nobody built a distributed monolith by splitting too little.
- Splitting relocates complexity; it doesn't remove it. Essential complexity moves out of the module and into the gaps between modules, where your usual tools aren't looking.
- Cohesion and coupling trade against each other. Maximising either one eventually hurts the other. Modularity is a balance point, not a score to maximise.
- Coupling counts, connascence grades. One connection carrying a shared algorithm can be worse than six connections carrying simple method calls. Coupling metrics alone can't tell you that.
- All three lenses see structure, not intent. They're instruments. Someone still has to interpret what they're showing you.
Further Reading
- Fundamentals of Software Architecture, 2nd edition, Mark Richards and Neal Ford: Talks more about cohesion and coupling, goes in depth with connascence.
- Structured Design, Yourdon and Constantine: predates object orientation entirely, which is part of why it's worth reading. Much of the cohesion and coupling vocabulary we still use was already here.
- Balancing Coupling in Software Design, Vlad Khononov: a different way of thinking about coupling, based on knowledge, volatility, and distance rather than connascence types