Coupling, Cohesion and Dependency Direction
Learning Objectives
Define coupling and cohesion and their kinds.
Use afferent/efferent coupling and instability to reason about dependency direction.
Detect and break dependency cycles; use layering, facades and adapters.
Prerequisites: Modules 19–20.
Intuition and Definition
Coupling — how much one module depends on another. Tight coupling (knows internals, shares mutable state) means changes ripple.
Cohesion — how closely the parts of one module belong together. High cohesion: everything in Invoice is about invoices.
Goal: high cohesion, low coupling. Robert C. Martin's package metrics:
Ca (afferent coupling) how many modules depend on this one (incoming) Ce (efferent coupling) how many modules this one depends on (outgoing) I = Ce / (Ca + Ce) (instability)
0 = stable (hard to change, many dependents), 1 = unstable (easy to change)
Stable Dependencies Principle: depend in the direction of stability — volatile modules (UI, adapters) should depend on stable ones (domain), not the other way round.
Why It Matters
Dependency cycles make it impossible to build, test or reuse a module alone, and turn small changes into large ones. Direction matters more than count.
Syntax and Language Notes
Language mechanisms that reduce coupling: interfaces, forward declarations and the Pimpl idiom (C++), package-private/internal visibility (Java/C#), module exports (TypeScript), and __all__/leading underscores (Python).
Complete Example
A tiny analyser that computes Ca, Ce and instability for a module graph and detects a cycle — the same analysis tools like madge, NDepend or JDepend perform.
deps = { # module -> modules it imports
"ui": ["app"],
"app": ["domain", "persistence"],
"persistence": ["domain"],
"domain": [],
"reports": ["domain", "app"],
}
def metrics(graph):
ca = {m: 0 for m in graph}
for m, outs in graph.items():
for o in outs:
ca[o] += 1
for m in sorted(graph):
ce = len(graph[m])
inst = ce / (ca[m] + ce) if ca[m] + ce else 0.0
print(f"{m:12} Ca={ca[m]} Ce={ce} I={inst:.2f}")
def find_cycle(graph):
state = {}
def visit(m, path):
if state.get(m) == "active":
return path[path.index(m):] + [m]
if state.get(m) == "done":
return None
state[m] = "active"
for o in graph[m]:
c = visit(o, path + [m])
if c:
return c
state[m] = "done"
return None
for m in graph:
c = visit(m, [])
if c:
return c
return None
metrics(deps)
print("cycle:", find_cycle(deps))
deps["domain"] = ["persistence"] # someone imports the database from the domain…
print("cycle after change:", " -> ".join(find_cycl
…app Ca=2 Ce=2 I=0.50
domain Ca=3 Ce=0 I=0.00
persistence Ca=1 Ce=1 I=0.50
reports Ca=0 Ce=2 I=1.00
ui Ca=0 Ce=1 I=1.00
cycle: None
cycle after change: domain -> persistence -> domainStep-by-Step Walkthrough
domain has three dependents and no dependencies: Ca=3, Ce=0, I=0.00 — maximally stable. Good: business rules should be stable.
ui and reports have no dependents: I=1.00 — free to change.
Adding domain → persistence creates a cycle: the most stable module now depends on an infrastructure module, so database changes ripple into the domain and the two can't be built or tested separately.
The fix is dependency inversion: the domain defines a Repository interface; persistence implements it (Module 22, 72).
flowchart LR
ui --> app
app --> domain
app --> persistence
persistence --> domain
reports --> domain
reports --> appCommon Mistakes
Metric worship: chasing numbers instead of asking "what changes together?".
Low coupling via global state — modules look independent but share hidden data.
"Utils"/"Common" modules that everything depends on and that depend on everything.
Bidirectional dependencies between layers, often through callbacks registered from the "wrong" side.
Trade-offs and Alternatives
Breaking coupling adds indirection (interfaces, events, adapters). Facades simplify access to a subsystem; adapters translate foreign APIs at the boundary; events decouple publishers from subscribers at the cost of harder tracing.
Real-World Use
Large codebases enforce dependency rules in CI (ArchUnit for Java, NDepend for .NET, dependency-cruiser/madge for TypeScript, import-linter for Python, include-what-you-use and layering checks for C++).
Practice
Q1. (Easy) What is instability when Ca=0 and Ce=4?
Answer: I = 4 / (0 + 4) = 1.0 — maximally unstable.
Q2. (Medium) Name two ways to break the cycle domain ↔ persistence.
Answer: (1) Dependency inversion: domain owns a Repository interface, persistence implements it. (2) Move the shared concept into a third module both depend on (or use domain events).
Q3. (Advanced) Why can a module with low *count* of dependencies still be badly coupled?
Answer: Coupling strength matters, not just count: depending on another module's internals, shared mutable state, temporal coupling (call order), or data-format assumptions can make a single dependency very costly.
Interview Question
Q1. "What's the difference between coupling and cohesion, and how do they relate?"
Answer: Cohesion is *inside* a module — how related its responsibilities are; coupling is *between* modules — how much they depend on each other. They tend to move together: when unrelated responsibilities are lumped together (low cohesion), other modules must depend on that lump for different reasons, raising coupling. Splitting by reason-to-change raises cohesion and lowers coupling.
Quick Quiz
Q1. Which modules should be most stable?
Answer: Core domain/policy modules that many others depend on.
Q2. What does Ca count?
Answer: Incoming dependencies (modules that depend on this one).
Q3. Name a pattern that reduces coupling to a complex subsystem.
Answer: Facade (or Adapter at the boundary).
Summary
Aim for high cohesion and low coupling; direction of dependencies matters most.
Instability I = Ce/(Ca+Ce); depend toward stability; break cycles with inversion or extraction.
Related: Modules 19, 22, 37 (Facade), 71–73 (architecture).