Coupling and cohesion: balancing dependencies and focus
Measure and improve your design by reducing coupling between modules and increasing cohesion within them, with Java examples and practical metrics.
Introduction
Coupling and cohesion are two complementary lenses for deciding whether a set of classes forms a maintainable boundary. Coupling looks across modules; cohesion looks within one module. Use them together when a design is split into classes but changes still ripple or responsibilities still blur.
You refactor a class into three smaller ones. The tests are cleaner, the PR looks good, and you merge it. A week later, a teammate changes class A and class B breaks. Another week, someone adds a feature to class C and accidentally duplicates logic already in class A. The classes are separate, but the separation did not actually help.
The missing lens is coupling and cohesion. Splitting code into modules is not enough. You need to measure two things: how much your modules depend on each other (coupling), and how focused each module is on a single purpose (cohesion). These two metrics together tell you whether your design is genuinely modular or just spread across more files.
Coupling and cohesion are useful first diagnostic questions in a design review. High coupling can signal tangled boundaries; low cohesion can signal a junk-drawer module.
TL;DR / Mental Model
- Coupling is about the links between modules: how many assumptions one module makes about another.
- Cohesion is about the focus inside a module: whether its fields and operations support one clear responsibility.
- Aim for lower coupling and higher cohesion, but treat that as a design target rather than an absolute rule. A small orchestrator may coordinate several focused collaborators by design.
What Are Coupling and Cohesion
Coupling is the degree of interdependence between modules. When module A depends on module B's internal data structures, concrete classes, or execution order, they are tightly coupled. A change in B forces a change in A.
Cohesion is the degree to which elements inside a module belong together. A class with high cohesion has methods and fields that all serve one focused purpose. A class with low cohesion is a grab bag of unrelated responsibilities glued together by convenience.
Think of coupling and cohesion like rooms in a house. Coupling is how many pipes and wires run between rooms. Cohesion is whether each room has one clear function (kitchen, bedroom) or is a combo kitchen-bedroom-office that does nothing well. You want clean rooms with minimal shared plumbing.
Definitions and boundaries
Coupling is directional: module A may depend on module B even when B does not depend on A. It includes more than imports, such as shared state, call order, data formats, and assumptions about side effects. Cohesion is about related responsibilities, not simply class size; a larger module can still be cohesive if its elements serve one purpose.
The usual design target is low coupling, high cohesion. It is a trade-off, not a promise that every dependency should disappear. A coordinator can have several dependencies while remaining cohesive if its only responsibility is coordinating them, and a stable shared abstraction can have high fan-in without being a design problem.
In the left diagram, every module reaches into another module's internals, so a change in one can affect the others. In the right diagram, modules communicate through a shared interface. When the interface remains stable, an internal change can leave the other modules unaffected.
Types of Coupling
Coupling exists on a spectrum from tight (avoid) to loose (prefer). Each level describes a different kind of dependency between two modules.
| Type | What it means | Example | Severity |
|---|---|---|---|
| Content | Module A reaches into B's internals and modifies private fields or jumps into B's code | Using reflection to set a private field | Worst |
| Common | Multiple modules read/write the same global or static mutable state | Two services sharing a static HashMap | Very bad |
| Control | A passes a flag to B that dictates B's internal control flow | process(order, /* skipValidation */ true) | Bad |
| Stamp | A passes an entire object to B, but B only uses one field | Passing User when you only need userId | Moderate |
| Data | A passes only primitive or simple data that B needs | calculateTax(subtotal, taxRate) | Good |
| Message | A and B communicate via async messages with no direct method call | Publishing an OrderPlaced event to a queue | Best |
The practical rule: prefer the least constraining coupling that still expresses the contract. If you are passing a whole User object just so the method can read user.getId(), pass the ID directly. If you are calling a method with a boolean flag, consider splitting it into two intention-revealing methods.
In practice, common and control coupling deserve particular attention. Global mutable state and boolean flag parameters can hide assumptions that are difficult to see at a call site.
Types of Cohesion
Cohesion also exists on a spectrum, from coincidental (worst) to functional (best). Each level describes how related the elements inside a module are.
| Type | What it means | Example | Quality |
|---|---|---|---|
| Coincidental | Elements grouped by accident, no logical relationship | A Utils class with formatDate(), parseJson(), sendEmail() | Worst |
| Logical | Elements do similar things but are unrelated in purpose | An InputHandler that handles keyboard, mouse, and file input | Poor |
| Temporal | Elements grouped because they run at the same time | A Startup class that initializes the DB, loads config, and warms cache | Weak |
| Procedural | Elements grouped because they run in sequence | A method that validates, transforms, then logs (always in that order) | Okay |
| Communicational | Elements operate on the same data | A class that reads a file and computes stats on its contents | Decent |
| Sequential | Output of one element is input to the next | A pipeline where parse() feeds validate() feeds transform() | Good |
| Functional | Every element contributes to a single well-defined task | TaxCalculator with only tax-related methods and fields | Best |
The goal is functional cohesion: every field and every method in the class exists to serve one purpose. If you can describe what the class does without using the word "and," it probably has functional cohesion.
The 'Util' class smell
A class named StringUtils, MiscHelper, or CommonUtils often has coincidental cohesion. The elements may share a file without sharing a real responsibility. When you see this, ask what the methods actually have in common. If the answer is "nothing," split them into the classes that actually use them.
The Goal: Low Coupling, High Cohesion
Low coupling and high cohesion are not two separate goals. They reinforce each other. When a class has high cohesion (everything in it is tightly related), it often needs fewer dependencies on other classes. When classes are loosely coupled (communicating through narrow interfaces), each one has more room to stay internally focused.
The top row shows two "god classes" that do everything and depend on each other's internals. The bottom row shows focused classes that communicate through interfaces. The bottom design can be easier to test, change, and explain when the interfaces match real responsibilities.
A useful qualification is that small projects with one developer can tolerate moderate coupling. The cost tends to grow when teams grow, features multiply, and someone other than the original author has to read the code. That is when tight coupling can turn into a tax on change.
Implementation: Before and After
Here is a concrete example. The "before" code has tight coupling (classes reach into each other) and low cohesion (classes mix unrelated concerns). The "after" code fixes both.
// β Low cohesion: this class does report generation, email sending,
// and database logging. Three unrelated concerns in one place.
// β Tight coupling: directly constructs dependencies, accesses
// global config, passes control flags.
public class ReportManager {
// Common coupling: reading shared mutable global state
private static final Map<String, String> GLOBAL_CONFIG = AppConfig.getSettings();
public void generateAndSend(String reportType, boolean skipEmail) {
// Concern 1: report generation
String data;
if (reportType.equals("sales")) {
data = querySalesData();
} else if (reportType.equals("inventory")) {
data = queryInventoryData();
} else {
data = "Unknown report type";
}
String report = formatAsHtml(data);
// Concern 2: email sending (control coupling via skipEmail flag)
if (!skipEmail) {
var emailClient = new SmtpEmailClient(
GLOBAL_CONFIG.get("smtp.host"),
Integer.parseInt(GLOBAL_CONFIG.get("smtp.port"))
);
emailClient.send(
GLOBAL_CONFIG.get("report.recipient"),
"Report: " + reportType,
report
);
}
// Concern 3: audit logging
try (var conn = DriverManager.getConnection(GLOBAL_CONFIG.get("db.url"))) {
var stmt = conn.prepareStatement(
"INSERT INTO audit_log (action, details) VALUES (?, ?)"
);
stmt.setString(1, "REPORT_GENERATED");
stmt.setString(2, reportType);
stmt.executeUpdate();
} catch (SQLException e) {
System.err.println("Audit log failed: " + e.getMessage());
}
}
private String querySalesData() { return "sales data..."; }
private String queryInventoryData() { return "inventory data..."; }
private String formatAsHtml(String data) { return "<html>" + data + "</html>"; }
}Notice what changed. The skipEmail boolean flag (control coupling) is gone. Global config access (common coupling) is gone, replaced by constructor injection of only the values each class needs. Each class has one concern (functional cohesion) instead of three concerns stuffed into one method.
The orchestrator's generateAndSend method is three lines. Each line calls one focused collaborator. You can test the ReportGenerator with a fake ReportDataSource without setting up production infrastructure. You can test AuditLogger without generating a report.
Measuring Coupling
You can quantify coupling during code reviews and design discussions. Two practical metrics stand out.
Fan-in is the number of modules that depend on a given module. High fan-in means the module is widely used (like a utility or interface). This is usually fine, as long as that module has a stable API.
Fan-out is the number of modules a given module depends on. High fan-out means the module reaches into many other modules to do its job. This is a warning signal, not a verdict: a class with 15 imports may be a deliberate coordinator, but it deserves a look at its responsibilities.
| Metric | What it measures | Healthy range | Red flag |
|---|---|---|---|
| Fan-in | How many modules depend on me | High is okay (stable, reusable) | Low fan-in on a "shared" class (nobody uses it) |
| Fan-out | How many modules I depend on | Context-dependent | High count combined with mixed responsibilities |
| Instability | Fan-out / (Fan-in + Fan-out) | Stable: 0.0, Unstable: 1.0 | Core domain classes with instability > 0.5 |
The instability ratio, introduced by Robert C. Martin, gives you a single number between 0 and 1. A module at 0.0 is maximally stable (many depend on it, it depends on few). A module at 1.0 is maximally unstable (it depends on many, few depend on it). You want your core business logic to be stable and your adapters (controllers, gateways) to be unstable.
A practical heuristic is to run a quick dependency count on any class that "feels wrong" in a review. If fan-out is high for the codebase, inspect whether the class is a coupling magnet; splitting may help when its responsibilities are mixed.
Interview tip: mention fan-in/fan-out
When an interviewer asks how you evaluate a design, say: "I look at coupling and cohesion. Specifically, I check fan-out on service classes and inspect whether the dependencies represent one cohesive responsibility. A high count is a prompt to investigate, not an automatic reason to split." That is a concrete, measurable answer.
Design Implications and Trade-offs
Lower coupling and higher cohesion usually make changes easier to localize, but they are not achieved by splitting every class or adding an interface to every dependency. A boundary is useful when it groups a coherent responsibility behind a contract that is stable enough for its callers.
- Focused classes: Extract a responsibility when it has a distinct reason to change, data, or test seam. Keep related validation, state, and rules together when separating them would only scatter the logic.
- Orchestrators: A coordinator can have high fan-out intentionally. Its cohesion comes from coordinating a workflow; the collaborators should own the domain work.
- Interfaces and messages: They can reduce knowledge of implementation details, but they add indirection, contract maintenance, and sometimes eventual consistency. A direct call inside one module may be the clearer choice.
- Metrics: Fan-in, fan-out, and instability help identify candidates for review, not automatic pass/fail thresholds. Read the responsibilities and change patterns alongside the numbers.
An exception is a stable shared abstraction such as a small value type or domain interface. It may have high fan-in without being tightly coupled to each caller's internals. Conversely, two classes with few imports can still be tightly coupled through shared mutable state or an undocumented call order.
30-Second Explanation
Coupling measures how much modules know about and depend on one another; cohesion measures how well the elements inside one module belong together. A useful design target is low coupling and high cohesion: keep responsibilities focused and communicate through narrow, stable contracts. Check the trade-off, thoughβan orchestrator may have several dependencies by design, and extra interfaces can add needless indirection.
5-Minute Explanation
- Identify the change boundary. Ask which fields, methods, and rules should change together; those elements are candidates for one cohesive module.
- Identify the assumptions across the boundary. Direct field access, shared mutable state, control flags, concrete constructors, and undocumented call order all increase coupling.
- Prefer the smallest useful contract. Pass the data or behavior a collaborator needs, and hide implementation details behind an interface when substitution or a real architectural boundary matters.
- Use the report example as a guide: separate generation, notification, and auditing, then keep a small orchestrator to coordinate them. The coordinator's fan-out is intentional because its one job is coordination.
- Validate with metrics and context. Fan-in, fan-out, and instability reveal hotspots, but a number alone does not prove that a class should be split. Consider team size, change frequency, runtime boundaries, and the cost of added indirection.
Common Mistakes
1. Coupling through shared mutable state
Two classes that both read and write to the same static HashMap or singleton cache are coupled even if they never import each other. This is common coupling, and it is invisible in import graphs. The symptoms show up at runtime: race conditions, stale reads, and changes in one class that silently break the other.
The fix: eliminate shared mutable state. If two modules need access to the same data, inject a shared service with a clear API, or use immutable data structures that one module produces and the other consumes.
2. The "Util" class with coincidental cohesion
StringUtils, MiscHelper, CommonFunctions. These names are symptoms. The class exists because someone needed a method and did not know where to put it. Six months later, the class has 40 methods that share nothing except their inability to find a better home.
The fix: move each method into the class that actually uses it. If formatCurrency is always called from pricing code, it belongs in the pricing package, not in a generic utility class. If multiple packages need it, create a focused CurrencyFormatter class rather than a dumping ground.
3. Hiding coupling behind a service locator
Some teams avoid constructor injection and instead use a ServiceLocator.get(ReportGenerator.class) pattern. The class signatures look clean (no dependencies in the constructor), but the runtime coupling may be comparable and is hidden. You cannot see what a class depends on without reading through the method bodies.
Constructor injection is explicitly coupled but visibly so. You see every dependency in the constructor signature. That visibility is a feature, not a bug. When the constructor has 12 parameters, it is a loud signal that the class has too many responsibilities.
4. Over-decoupling with unnecessary interfaces
Creating an interface for every class (the IFoo / FooImpl pattern) does not necessarily reduce coupling if there is only one implementation and no current boundary requires substitution. It can add indirection without benefit. The IDE now shows you two files instead of one, while the coupling between the caller and the implementation may remain unchanged.
Create interfaces when you genuinely need polymorphism: multiple implementations, test doubles for expensive resources, or a dependency inversion boundary between layers. If you have one implementation and no plans for a second, skip the interface.
When to add an interface
Use this test: "Would I want a second implementation?" If the answer is "yes, for testing" (databases, HTTP clients, file systems), an interface may earn its keep. If the answer is "no, this is just internal logic," a concrete class may be simpler. You can extract an interface later when a concrete need arises.
Test Your Understanding
Quick Checks
- Question: What is coupling? Answer: The assumptions and dependencies between modules.
- Question: What is cohesion? Answer: How closely the elements inside one module support one responsibility.
- Question: Is high fan-out automatically bad? Answer: No; inspect whether the class is a cohesive coordinator or a mixed-responsibility hotspot.
- Question: What is the usual design target? Answer: Lower coupling and higher cohesion, balanced against clarity and the cost of extra indirection.
Quick Recap
- Coupling measures dependencies between modules; cohesion measures focus within a module. The usual target is lower coupling with higher cohesion.
- Coupling ranges from content coupling (worst, modifying another module's internals) to message coupling (best, async communication with no direct call).
- Cohesion ranges from coincidental (worst, random methods in a Util class) to functional (best, every element serves one purpose).
- Low coupling and high cohesion reinforce each other: focused modules naturally need fewer dependencies.
- Measure coupling with fan-in (who depends on me) and fan-out (who do I depend on). A high fan-out is a prompt to inspect responsibilities, not a universal failure threshold.
- The most common coupling mistake is shared mutable state, which creates invisible dependencies that do not appear in import graphs but cause runtime failures.
- The most common cohesion mistake is the "Util" class, which is a dumping ground of coincidental cohesion. Move each method to the class that owns that concern.
Related OOP Concepts
- Encapsulation: hides mutable state and prevents accidental content coupling.
- Abstraction and interfaces: narrow the contracts across which coupling is expressed.
- Single Responsibility Principle: a practical guide for improving cohesion.
- Composition: assembles focused collaborators without exposing their internals.
- Dependency Inversion and Dependency: help control the direction and strength of module relationships.
Related Articles
Six practical principles that guide clean code decisions: KISS, DRY, YAGNI, Law of Demeter, Single Level of Abstraction, and Principle of Least Astonishment.
Apply separation of concerns to keep each class, module, and layer focused on a single aspect of functionality, making code easier to change and test.
Encapsulation bundles data with the behavior that controls it. Private fields help enforce invariants when all mutation paths respect the object's rules.