Coupling & Cohesion
Two measures of design quality: coupling is how dependent modules are on each other; cohesion is how focused a single module is. Aim for low coupling and high cohesion.
Dependency and focus.
Coupling measures how tightly two modules depend on each other. Cohesion measures how well the parts inside one module belong together. The goal is low coupling (modules can change independently) and high cohesion (each module does one thing well).
Analogy: well-designed appliances are highly cohesive (a toaster only toasts) and loosely coupled (you can replace your toaster without touching your fridge). A gadget that toasts, brews coffee, and charges your phone is low-cohesion; if it also can't work unless your specific fridge is present, it's tightly coupled.
# Problem: one class does many jobs AND digs into another class's guts. class OrderService: def place(self, order, user): # Low cohesion: pricing + email + DB in one place total = 0 for item in order.items: total += item.price * (1 - user.profile.discount_rate) db.orders.insert(order.__dict__) smtp.send(user.email, f"total={total}") # Tight coupling: knows User.profile.discount_rate internals
# Fix: high cohesion (one job each) + low coupling (talk via small APIs). class Pricing: def total(self, order, discount): return sum(i.price for i in order.items) * (1 - discount) class OrderRepository: def save(self, order): ... class Mailer: def send_receipt(self, email, total): ... class OrderService: def __init__(self, pricing, repo, mailer): self.pricing, self.repo, self.mailer = pricing, repo, mailer def place(self, order, email, discount): # Depends on collaborators' public methods — not their fields. total = self.pricing.total(order, discount) self.repo.save(order) self.mailer.send_receipt(email, total)
Loose coupling, high cohesion.
- Tight coupling signals — one class knows another's internals, changes ripple, hard to test in isolation.
- Loose coupling tools — interfaces, dependency injection, events/messages.
- High cohesion — a class's methods and fields all serve one clear responsibility (aligns with the Single Responsibility Principle).
- Low cohesion signals — a "utility"/"manager" class doing unrelated things.
Types of coupling and cohesion.
- Coupling types (worst→best): content, common, control, stamp, data.
- Cohesion types (worst→best): coincidental, logical, temporal, procedural, communicational, sequential, functional.
- Metrics — afferent/efferent coupling, instability, LCOM (lack of cohesion of methods).
- Stable dependencies — depend in the direction of stability; abstractions should be stable, details volatile.
Interview Questions
A measure of how strongly one module/class depends on another. High (tight) coupling means changes in one force changes in the other; low (loose) coupling means they can evolve independently.
A measure of how closely related and focused the responsibilities within a single module/class are. High cohesion means everything in the module works toward one well-defined purpose.
Low coupling and high cohesion. Modules should be self-contained and focused (high cohesion) while depending minimally on each other (low coupling), which makes the system easier to understand, change, test, and reuse.
A class directly instantiates and reaches into the internals of another (accessing its fields or calling long method chains), so you can't change or test one without the other. Needing to modify many files for one logical change is another symptom.
A class that does many unrelated things — a grab-bag "Utils" or "Manager" class handling file I/O, formatting, and network calls. Its methods don't share a common purpose or data.
It lets modules change, be replaced, or be tested independently, reduces the ripple effect of changes, enables reuse, and makes the system easier to understand one piece at a time.
Programming to interfaces plus dependency injection — depend on an abstraction and have concrete implementations supplied from outside. Events/message passing and mediators also decouple components.
They tend to move together in a good direction: when you group related responsibilities into a cohesive module, the interactions that used to cross module boundaries become internal, reducing coupling. Conversely, low cohesion (a module doing many things) usually forces it to depend on many others, raising coupling. Improving one often improves the other, which is why "high cohesion, low coupling" is stated as a pair.
SRP ("a class should have one reason to change") is essentially a statement about maximizing cohesion at the class level: if a class has a single, well-defined responsibility, its members are all related to that responsibility (high cohesion) and there's only one axis of change. Violating SRP produces low-cohesion classes that mix concerns and change for multiple unrelated reasons.
By letting a client depend on an abstract contract instead of a concrete class, interfaces remove the client's knowledge of implementation details. The concrete class can change or be swapped (including for a test double) without affecting the client, and the dependency is narrowed to just the declared methods. This turns a rigid, direct dependency into a flexible, substitutable one.
DI removes a class's responsibility for creating its own dependencies by supplying them from outside (constructor/setter/parameter). The class no longer references concrete classes via new, so it's decoupled from specific implementations and can receive any that satisfy the interface — real ones in production, fakes in tests. This inverts control, centralizes wiring, and makes dependencies explicit and swappable.
Afferent coupling (Ca): the number of other modules that depend on this module (incoming). Efferent coupling (Ce): the number of modules this module depends on (outgoing). High afferent coupling means many things rely on you, so you should be stable; high efferent coupling means you rely on many things, making you sensitive to their changes. They feed the instability metric.
Modules must collaborate, so some coupling is necessary and healthy. Chasing zero coupling can lead to over-abstraction: excessive indirection, event soup, or anemic modules that don't do anything meaningful together, making the system harder to follow. The goal is appropriate coupling — necessary, explicit, and through stable interfaces — not the absolute minimum. Decoupling that adds more complexity than it removes is counterproductive.
It restricts a method to talking only to its immediate collaborators (itself, its fields, its parameters, objects it creates), forbidding chains like a.getB().getC().doIt(). Such chains couple you to the internal structure of B and C; if that structure changes, your code breaks. By adding a method on the direct collaborator that encapsulates the chain, you depend only on it, lowering coupling and improving encapsulation — at the cost of some extra delegating methods.
From tightest/worst to loosest/best: content coupling (one module modifies or relies on another's internals directly) → common coupling (modules share global state) → external coupling (shared external format/protocol) → control coupling (one passes a flag telling another what to do) → stamp coupling (passing a whole record when only part is needed) → data coupling (passing only the simple data actually needed). The goal is to move toward data coupling — modules communicate through minimal, explicit parameters — and avoid content/common coupling, which create hidden, brittle dependencies.
From lowest/worst to highest/best: coincidental (unrelated things thrown together) → logical (grouped by category, selected by a flag) → temporal (done at the same time, e.g. "init" routines) → procedural (steps in a sequence) → communicational (operate on the same data) → sequential (output of one is input to the next) → functional (all elements contribute to a single well-defined task). Aim for functional cohesion — a module doing exactly one thing — which is the most maintainable and reusable.
Instability I = Ce / (Ca + Ce), where Ce is efferent (outgoing) and Ca afferent (incoming) coupling. I ranges 0 (maximally stable — many depend on it, it depends on nothing) to 1 (maximally unstable — depends on much, nothing depends on it). Robert Martin's Stable Dependencies Principle says dependencies should point toward more stable modules (I should decrease in the direction of dependency), and the Stable Abstractions Principle says stable modules should be abstract (so they can be extended without modification). Plotting abstractness vs instability reveals modules in the "zone of pain" (stable + concrete, hard to change) or "zone of uselessness" (abstract + unstable).
LCOM is a metric estimating how cohesive a class is by analyzing how its methods share instance fields. Intuitively, if methods can be partitioned into groups that use disjoint sets of fields, the class is really several classes glued together (low cohesion, high LCOM). A common variant (LCOM4) counts the connected components formed by methods linked through shared fields or calls — more than one component suggests the class should be split. High LCOM flags candidates for the Extract Class refactoring, though metrics are heuristics, not absolute verdicts.
The same principles scale up. A well-designed service is highly cohesive — it owns a single business capability/bounded context and its data — and loosely coupled to others, communicating through stable, coarse-grained contracts (APIs/events) rather than shared databases or chatty synchronous calls. Sharing a database (common coupling) or reaching into another service's internals recreates content coupling across the network. Poor boundaries produce a "distributed monolith": services that must be deployed together because they're tightly coupled, combining the downsides of both worlds. Domain-driven design's bounded contexts are essentially a cohesion/coupling exercise for service boundaries.
Publishing events (or using a message bus) lets a producer notify interested parties without knowing who they are — removing direct, compile-time dependencies and enabling parts to evolve and scale independently (temporal and referential decoupling). However, it introduces new, subtler coupling: consumers depend on the event schema/contract (schema coupling), and the system now has implicit workflows spread across handlers that are harder to trace, plus eventual-consistency and ordering concerns. So you trade explicit structural coupling for contract coupling and reduced visibility. Managing it requires versioned, well-documented event schemas, schema registries, and good observability/tracing.
Analyze responsibilities and how methods cluster around shared fields (LCOM-style). Extract each cohesive cluster into its own class (Extract Class), giving it a clear single responsibility and moving the relevant state and behavior together. Replace the god class's internal calls with delegation to the new collaborators, injecting them via interfaces to keep coupling loose. Introduce a thin facade if callers need a single entry point during transition. Do it incrementally behind tests: extract one responsibility, verify, repeat, watching that you don't create new tight coupling (e.g. new classes reaching into each other's internals). The end state is several highly-cohesive classes with minimal, interface-based dependencies, which is far easier to test and change than the monolith.
Global mutable state (or a shared singleton holding data) is a form of common/content coupling: any module can read or modify it, creating invisible dependencies that don't appear in interfaces or signatures. Changes have action-at-a-distance effects, ordering and concurrency bugs become likely (unsynchronized shared access), and testing is hard because state leaks between tests and behavior depends on hidden context. Because the coupling is implicit and widespread, it's especially brittle. The remedy is to make dependencies explicit — pass state through parameters or inject it — and prefer immutability, so collaboration happens through clear contracts rather than a shared global blackboard.
No. Modules must interact, so some coupling is necessary. Low coupling means keeping those dependencies few, explicit, and through stable interfaces — not eliminating them. The aim is minimal, well-managed coupling, not zero.
Coupling is between modules (how much they depend on each other). Cohesion is within a module (how focused and related its contents are). Remember: coupling = inter-module, cohesion = intra-module.
A highly cohesive module does one well-defined thing, so it can be dropped into another context that needs exactly that capability without dragging along unrelated concerns or dependencies. A low-cohesion module bundles many responsibilities, so reusing part of it forces you to take (and depend on) the rest, discouraging reuse. Focused modules are also easier to understand, name, and trust.
Control coupling is when one module passes a flag/argument that dictates the internal control flow of another (e.g. process(data, mode) where mode makes it behave completely differently). It's undesirable because the caller must know the callee's internal logic, the callee becomes a low-cohesion multi-behavior method, and changes to one behavior risk others. It often signals that the method should be split into separate, cohesive methods, or that polymorphism should replace the flag.
Qualitative signals: a change requires edits across many files (high coupling); a class has a vague name like "Manager/Helper/Util" and unrelated methods (low cohesion); heavy use of another class's getters or long call chains (Law of Demeter violations); circular dependencies. Quantitative tools: static analyzers reporting afferent/efferent coupling, instability, dependency graphs, and LCOM (lack of cohesion of methods); architecture-fitness tests (ArchUnit) to enforce dependency rules. Metrics are heuristics that point to candidates for Extract Class / introduce-interface refactorings, not absolute judgments.
Connascence (Meilir Page-Jones) is a finer-grained taxonomy of coupling describing how two pieces of code are connected such that changing one requires changing the other. It has static forms (connascence of name, type, meaning, position, algorithm) and dynamic forms (execution order, timing, value, identity), and each has a degree and locality. Guidance: prefer weaker forms (name/type over position/meaning), keep any strong connascence local (within a small scope), and reduce degree. It refines "reduce coupling" into concrete, rankable transformations — e.g. replacing positional arguments (connascence of position) with named parameters (connascence of name) is a measurable improvement.
Usually they align, but they can conflict. Splitting a class to raise cohesion (one responsibility each) can increase the number of inter-module dependencies and the coordination/communication between the new pieces — potentially raising coupling if the split is along the wrong seam. Conversely, merging things to reduce inter-module calls can hurt cohesion. The skill is finding boundaries where responsibilities are genuinely separable so that each resulting module is cohesive and the interactions across the boundary are minimal and clean (a natural "seam"). Bad boundaries produce both low cohesion and high coupling; good boundaries (often domain-aligned) optimize both. So it's about where you cut, not blindly maximizing one metric.
Layering restricts dependencies to flow in one direction (e.g. presentation → application → domain), preventing tangles and cycles. But naive layering couples high-level policy to low-level details (the domain depending on the database). The Dependency Inversion Principle fixes the direction: high-level modules define interfaces (ports) that low-level modules implement (adapters), so at runtime the domain depends only on abstractions it owns, and concrete details depend inward on those abstractions. This is the basis of hexagonal/clean architecture: it keeps the stable, valuable core decoupled from volatile infrastructure, so databases, frameworks, and I/O can change without touching business logic. Coupling still exists but points toward stable abstractions rather than volatile implementations.