Component Boundaries: Cohesion, Coupling, Afferent/Efferent
What This Concept Is
Cohesion and coupling are the two oldest measures of modularity. They predate almost every architecture style in this module.
- Cohesion measures how much the elements inside a component belong together. High cohesion = the component has one reason to change.
- Coupling measures how much components depend on each other. Low coupling = one component can change without the others changing.
The rough goal: high cohesion within a component, low coupling between components.
Cohesion, ranked best to worst
| Type | Meaning | Keep or split? |
|---|---|---|
| Functional | All parts contribute to one well-defined task | Keep -- ideal |
| Sequential | Output of one part is input to another | Keep |
| Communicational | Parts operate on the same data | Often keep |
| Procedural | Parts execute in order, weakly related | Consider splitting |
| Temporal | Parts execute at the same time (init()) | Split if possible |
| Logical | Parts are of "the same kind" (all utils) | Usually split |
| Coincidental | Parts ended up together by accident | Always split |
Coupling, simplest to strictest
- Afferent coupling (Ca): how many other components depend on this component? ("incoming" --
afor "arriving") - Efferent coupling (Ce): how many other components does this component depend on? ("outgoing" --
efor "exiting") - Instability (I) = Ce / (Ca + Ce): ranges 0 (maximally stable, many depend on it, it depends on nothing) to 1 (maximally unstable, depends on many, nothing depends on it).
- Abstractness (A): fraction of abstract classes/interfaces in the component.
- Distance from the main sequence (D):
|A + I - 1|. Zero is good: abstract components should be stable; concrete ones should be unstable.
Robert C. Martin's rule of thumb
Stable components (low I) should be abstract (high A). Unstable components (high I) should be concrete (low A). Components off the line A + I = 1 are in the zone of pain (concrete and stable -- hard to change) or the zone of uselessness (abstract and unstable -- nothing uses them).
A
1 +----- zone of uselessness -----+
| |
| main sequence |
| (A + I = 1) |
| |
0 +------- zone of pain ----------+
0 1
I
Why It Matters Here
This is the measuring stick for Clusters 2, 3, and 4. Every later style (modular monolith, service-based, microservices) is an exercise in choosing component boundaries. If the boundaries have low cohesion and high coupling, the style does not matter -- the system will hurt whatever it is called.
You will use these metrics in two places:
- Reviewing an existing system. Compute Ca/Ce per package. Packages in the zone of pain are top candidates for refactoring. Packages in the zone of uselessness are candidates for deletion or merger.
- Designing a new decomposition. Before splitting a modular monolith into microservices (Cluster 4), check that the proposed modules have high cohesion inside and low coupling between. If not, splitting will make things worse.
Concrete Example
Hypothetical OrderFlow module graph, with Ca/Ce counts:
Module Ca Ce I = Ce/(Ca+Ce) A Verdict
-------------- --- --- --------------- --- ---------------
catalog.api 8 0 0.00 0.7 stable + abstract - OK
catalog.domain 2 4 0.67 0.1 unstable + concrete - OK
checkout.api 3 2 0.40 0.6 moderate - OK
checkout.domain 1 5 0.83 0.0 unstable + concrete - OK
shared.utils 9 6 0.40 0.0 concrete + stable - ZONE OF PAIN
fulfillment.api 0 3 1.00 0.8 abstract + unstable - ZONE OF USELESSNESS
Reading the table:
catalog.apiis correctly stable (many depend on it) and abstract (mostly interfaces). Good.shared.utilsis concrete (just functions) but 9 components depend on it. Changes to it ripple widely. Break it up or freeze its surface.fulfillment.apiis abstract but nobody is calling it. Either the integration was never wired, or the abstraction is premature.
This is what Richards and Ford call "measuring modularity" -- not a replacement for judgment, but a fast objective pass that tells you where to look.
Cohesion example at the method level
class OrderService:
def create_order(self, ...): ... # creates order
def cancel_order(self, ...): ... # cancels an existing order
def get_customer_loyalty_tier(self, ...): ... # ?!
def send_marketing_email(self, ...): ... # ?!!
The first two methods are functionally cohesive. The last two are coincidental -- they ended up here because someone needed somewhere to put them. OrderService has at least three reasons to change: order lifecycle logic, loyalty rules, marketing logic. Split.
Common Confusion / Misconception
"Coupling is always bad." Coupling is the point of software -- modules have to talk to each other to do anything. The bad kind is excess or bidirectional coupling. A dependency from checkout to catalog.api is necessary. A dependency from catalog.domain to checkout.domain is a red flag: catalog should not know about checkout.
"Cohesion and SRP are the same thing." The Single Responsibility Principle is a class-level heuristic for functional cohesion. Cohesion is a broader concept that also applies to modules, packages, and services. Your service can satisfy SRP at every class and still have terrible cohesion at the module level.
"A + I = 1 is an ideal to hit exactly." It is not. Distance from the main sequence is a signal, not a target. Components can sit somewhat off the line for good reasons. The purpose is to identify outliers, not to score every component.
"Connascence replaces coupling." Connascence is a finer-grained taxonomy of coupling (name, type, meaning, position, algorithm, identity, etc.). It is useful when you are debating between two refactor options. For module-level analysis, afferent/efferent counts are sufficient and simpler.
How To Use It
A 1-hour modularity audit of any codebase:
Tools that compute Ca/Ce automatically:
- Java / JVM: JDepend, Sonar, ArchUnit (metrics)
- C#: NDepend
- Python:
pydeps,import-linter - JavaScript / TypeScript:
madge,dependency-cruiser
For languages without a great tool, a 30-line script that walks import statements and counts per-package is enough for an 80% pass.
Check Yourself
- A package has
Ca = 0, Ce = 7. Is it stable or unstable? Is this a problem? What would you check next? - Explain the zone of pain in one sentence. Name one example from your own experience.
- When would you deliberately tolerate a component with
Ca = 1, Ce = 1? (Hint: small features that only one caller uses.)
Mini Drill or Application
Pick a real codebase. In 30 minutes:
- List 6-10 packages or modules.
- For each, estimate Ca and Ce by skimming imports.
- Flag any in the zone of pain or zone of uselessness.
- For each flag, write one sentence: "refactor idea" or "tolerate because X."
- Identify one method or class with obviously coincidental cohesion. Propose the split.