Coupling & Cohesion: Measuring What Makes a Design Good
The two qualities that decide whether a codebase stays easy to change: low coupling and high cohesion, with concrete metrics, code examples, and refactoring techniques.
Coupling & Cohesion
SOLID and the OOP pillars are tools; coupling and cohesion are the measurements that tell you whether you used them well. Every "good design" claim ultimately cashes out to: does this module depend on too much (coupling), and does everything inside this module actually belong together (cohesion)? Interviewers probe both directly — "how would you reduce coupling here?" is one of the most common LLD follow-up questions.
1. Coupling: How Much One Module Depends on Another's Internals
Coupling isn't binary — it's a spectrum from "barely depends on anything about the other module" to "depends on its literal internal fields." Classic software engineering literature (Structured Design, still the clearest taxonomy for interviews) ranks coupling types from worst to best:
Content coupling — the worst kind
// One module reaches directly into another's internal state
class OrderProcessor {
void process(Order order) {
order.items.remove(0); // reaching into another object's internal list directly
// any change to Order's internal representation (e.g. items → Map) breaks this
}
}Common coupling — shared mutable global state
// Two unrelated classes silently coupled through a static field
class InventoryManager {
static int stockCount; // global mutable state
void reduceStock() { stockCount--; }
}
class ReportGenerator {
void printStock() {
System.out.println(InventoryManager.stockCount); // implicit coupling to InventoryManager's timing
}
}Control coupling — a flag that dictates the callee's internal branching
// VIOLATION: caller must know the callee's internal logic to pick the right flag
void generateReport(boolean isSummary) {
if (isSummary) { /* summary logic */ } else { /* detailed logic */ }
}
generateReport(true); // caller must know what "true" means internally
// BETTER: separate methods, or a Strategy — the caller expresses INTENT, not internal control flow
void generateSummaryReport() { /* ... */ }
void generateDetailedReport() { /* ... */ }Stamp coupling — passing more than is needed
// Passes an entire Customer when only the email is used — couples this method
// to Customer's full shape, and it breaks if Customer's constructor changes.
void sendReceipt(Customer customer, Order order) {
email(customer.getEmail(), order);
}
// BETTER: data coupling — pass exactly what's needed
void sendReceipt(String email, Order order) {
email(email, order);
}Stamp coupling is not always wrong — passing a well-defined value object (Money, Address) is often clearer than exploding it into five primitive parameters. The rule of thumb: stamp coupling is fine when the whole object is conceptually relevant; it's a smell when only one unrelated field is actually used.
Data coupling — the goal
// Only primitive/value data that's actually needed crosses the boundary
double calculateTax(double amount, double taxRate) {
return amount * taxRate;
}2. Cohesion: Does Everything Inside a Module Actually Belong Together?
Cohesion measures how closely the responsibilities within a single module relate to each other. Same taxonomy tradition, ranked best to worst:
| Cohesion type | What it looks like | Why it's a problem (if low) |
|---|---|---|
| Functional (best) | TaxCalculator.calculate() — every line serves one purpose | N/A — this is the target |
| Sequential | A pipeline class where step 2 needs step 1's output | Fine if steps are genuinely one pipeline; risky if forced into one class |
| Communicational | Methods that all read/write the same record, different purposes | Borderline — often fine, watch for growth |
| Procedural | Methods grouped because "they run in this order in main()" | Order-dependent, easy to break by reordering |
| Temporal | A Utils.initEverything() doing DB setup + logging + config, because they all "happen at startup" | No logical relationship beyond timing — a new engineer can't guess what's safe to remove |
| Logical | handle(String type) with a switch that does unrelated things per type flag | Adding a case means editing a shared method — an OCP violation too |
| Coincidental (worst) | A Utils class with formatDate(), calculateTax(), sendEmail() | Literally a dumping ground — zero shared purpose |
// LOW cohesion: a "Utils" class that groups unrelated operations
// (logical/coincidental — the only thing these methods share is "we didn't know where else to put them")
class OrderUtils {
double calculateTax(double amount) { return amount * 0.08; }
void sendConfirmationEmail(String email) { /* SMTP */ }
String formatOrderId(int id) { return "ORD-" + id; }
boolean isWeekend(LocalDate date) { return date.getDayOfWeek() == DayOfWeek.SATURDAY; }
}// HIGH cohesion: each responsibility gets its own functionally-cohesive class
class TaxCalculator {
double calculate(double amount) { return amount * 0.08; }
}
class OrderConfirmationMailer {
void send(String email, Order order) { /* SMTP */ }
}
class OrderIdFormatter {
String format(int id) { return "ORD-" + id; }
}Improving cohesion: three refactoring moves
| Technique | What it does | When to use |
|---|---|---|
| Extract Method | Pull a fragment of a long method into its own named method | A method does two things — split into two methods, each doing one |
| Extract Class | Move a cluster of related fields+methods into a new class | A class has two clusters of fields that are never used together |
| Move Method | Move a method to the class whose data it actually uses most | A method on A mostly reads fields off a parameter of type B — it probably belongs on B |
3. Fan-In, Fan-Out, and Dependency Graphs
Coupling isn't just pairwise — at a system level, you measure it with fan-out (how many other modules does this module depend on?) and fan-in (how many other modules depend on this one?).
High fan-out (a module depending on many others) signals a class trying to do too much coordination — often an SRP violation. High fan-in (many modules depending on one) isn't automatically bad — a well-designed shared abstraction (like a core Money value type) should have high fan-in — but it means that module's API is now very expensive to change, since every caller is affected.
Circular dependencies
A circular dependency — module A depends on B, and B depends (directly or transitively) back on A — is one of the most damaging coupling patterns: neither module can be understood, tested, or deployed independently of the other.
// Circular dependency: OrderService needs PricingService, and
// PricingService needs OrderService (for order history) — neither
// can be constructed, tested, or understood alone.
class OrderService {
PricingService pricingService;
}
class PricingService {
OrderService orderService; // cycle!
}Breaking the cycle — extract the shared need into a third component both depend on, one-directionally:
// FIXED: extract what PricingService actually needs (order history)
// into its own read-only abstraction. Both classes now depend
// DOWNWARD on it; neither depends on the other.
interface OrderHistoryReader {
List<Order> pastOrders(CustomerId id);
}
class OrderService implements OrderHistoryReader {
public List<Order> pastOrders(CustomerId id) { /* ... */ return List.of(); }
}
class PricingService {
private final OrderHistoryReader historyReader; // depends on the abstraction, not OrderService directly
PricingService(OrderHistoryReader historyReader) { this.historyReader = historyReader; }
}This is Dependency Inversion (from the SOLID guide) applied specifically to break a cycle: introduce an abstraction that only one side needs to depend on downward, instead of two concrete classes depending on each other directly.
4. Coupling and Cohesion Pull in the Same Direction
They aren't independent axes to balance — they reinforce each other. A class with low cohesion (doing several unrelated things) almost always has high coupling too, because each of its unrelated responsibilities drags in its own set of dependencies.
Interview Questions
- Rank the coupling types from worst to best and give a code example of the worst kind.
- What's the difference between stamp coupling and data coupling? Is passing a whole object always a smell?
- Explain functional cohesion vs coincidental cohesion with an example of each.
- How would you refactor a
Utilsclass full of unrelated static methods to improve cohesion? - What do fan-in and fan-out measure, and why is high fan-in not automatically a bad sign?
- How do you detect and break a circular dependency between two services?
- Why do low cohesion and high coupling tend to appear together in the same class?