Overview
Coupling measures how much one module depends on the internals, timing, or availability of another; cohesion measures how strongly the elements inside a module belong together. They are the oldest pair of forces in software design (Constantine & Yourdon, structured design, 1970s) and still the vocabulary behind nearly every architectural argument: good boundaries are the ones that keep cohesion high inside and coupling low across. Almost every modern pattern — layering, hexagonal ports, bounded contexts, microservices — is a strategy for managing this trade-off, not an escape from it.
Key points
- Low coupling ≠ no coupling. Modules must connect to be useful; the goal is depending on stable, narrow, explicit contracts rather than on internals. Afferent/efferent coupling and the stable-dependencies principle give it a measurable form.
- Kinds of coupling, worst to best (structured-design lineage): content (reaching into internals) → common (shared global state) → control (passing flags that steer the callee) → stamp (passing whole structures for one field) → data (passing just what's needed). Modern additions: temporal coupling (must run in order / at the same time) and deployment coupling (must ship together) — the two that distributed systems make expensive.
- Kinds of cohesion, worst to best: coincidental (utility grab-bags) → logical →
temporal → procedural → communicational → sequential → functional (one reason to
exist). "God classes" and
utils/folders are cohesion failures before they are size problems. - Cohesion decides where code lives; coupling decides how it talks. A boundary drawn through a cohesive cluster converts cheap in-process calls into expensive cross-boundary coupling — the classic distributed-monolith failure.
- Connascence (Meilir Page-Jones) refines the vocabulary: two elements are connascent when changing one forces changing the other; prefer weaker (name, type) over stronger (order, timing, values), and keep strong connascence local.
- Heuristics: put things that change together, together (common closure); depend in the direction of stability; make coupling explicit (interfaces, events, contracts) so it can be seen and tested; measure change-coupling in version history, not just imports.
Practice
- Connascence hunt (source) — pick one module and classify every connascence instance you find, then refactor the strongest (position, timing, values) down toward the weakest (name, type); recognition drill for invisible coupling.
- Trip Service kata (source) — the code can't even be tested until you break its coupling to statics and singletons; the best hands-on lesson on why coupling direction matters.
- Racing Car katas (source) — each small exercise is rigged so one coupling violation blocks testability; fix the coupling to get the test in.
- ArchUnit fitness tests (source) — encode your intended dependency directions as executable tests on a real codebase and watch what fails: coupling made visible and enforceable.
- Change-coupling archaeology (source) — mine a repo's git history (code-maat or CodeScene) for files that always change together but live apart — cohesion failures no import graph will show; the exercises from Tornhill's Your Code as a Crime Scene.
- Gilded Rose — legacy rescue (exercise) — the flagship cohesion workout: extract a design from one god function, under tests.
- Birthday Greetings — hexagonal extraction (exercise) — the coupling workout at architecture scale: start from a deliberately coupled script and cut the boundary — cohesive pure core behind ports, I/O pushed into adapters.
Related
- Clean code — SOLID is largely coupling/cohesion at class scale (SRP is cohesion; DIP manages coupling direction).
- Hexagonal architecture — ports keep domain↔infrastructure coupling narrow and directed inward.
- DDD — bounded contexts are cohesion boundaries drawn on the business domain.
- Refactoring techniques — most catalog moves raise cohesion or loosen coupling.
- Microservice architecture — gets its cost model from deployment and temporal coupling across the network.
- Covariance & contravariance — variance annotations narrow what a contract exposes, shrinking what clients can couple to.
- Patterns for tools that generate configuration — "render the target's native format" is a coupling decision about whose contract you depend on.