Cohesion & Coupling
In simple words
These two ideas judge how well code is split into pieces. Cohesion is about the inside of one piece: does everything in it belong together and do one clear job? The higher, the better. Coupling is about the link between two pieces: how much does one need to know about the other? The lower, the better. Good design has high cohesion and low coupling.
Cohesion — one module, one job
A class or service should have one clear responsibility, and everything in it should serve that responsibility.
High cohesion — good
class UserService {
createUser();
updateUser();
deleteUser();
getUser();
}
Everything is user management. One reason to change.
Low cohesion — bad
class UserService {
createUser();
sendEmail(); // notifications
generatePdf(); // reporting
uploadImage(); // storage
processPayment(); // billing
}
Five unrelated responsibilities in one class. A change to the email provider, the PDF library, or the payment logic all land in UserService. It's hard to test, hard to reason about, and it violates the Single Responsibility Principle.
Signal: if you describe a class with "it does X and Y and Z", cohesion is low — split it.
Coupling — how much one module leans on another
High coupling — bad
class OrderService {
private CardPaymentService payment = new CardPaymentService(); // names a concrete class + builds it
}
OrderService now knows the exact class, its constructor, and its behaviour. Support UPI → edit OrderService. Support wallet → edit again. Swap payment library → edit again.
Low coupling — good
interface PaymentService { void pay(Order order); }
class OrderService {
private final PaymentService payment; // knows only the interface
OrderService(PaymentService payment) { this.payment = payment; }
}
OrderService knows one small contract: pay(Order). Any implementation can be handed in. Implementations change freely; OrderService doesn't move.
The lever: depend on an interface, not a concrete class, and let the dependency be handed in from outside (that's Dependency Injection).
The golden rule — high cohesion + low coupling
UserService → only user operations (high cohesion)
PaymentService → only payment operations (high cohesion)
NotificationService → only notifications (high cohesion)
They talk via APIs / events, never by reaching into each other (low coupling)
The opposite — the thing to avoid:
UserService: creates users, sends email, does payments, writes PDFs,
reads the payment DB, reads the inventory DB ...
That's a god class (or god service): hard to maintain, hard to scale, hard to test, changed by everyone.
At system-design scale
This is the argument for how you split a system:
- Microservices = cohesion at the service level. Each service owns one domain (orders, payments, inventory, notifications) and can be built, deployed, and scaled on its own.
- "No service reads another service's database" = low coupling. Services talk through APIs or Kafka events; internal schemas stay private, so a schema change in Payments can't break Orders.
- API Gateway, message queues, interfaces between layers — all exist to keep coupling low so parts can change independently.
Order Service ──REST/Kafka──► Payment Service ──► Payment DB
│
└── knows only the Payment API contract, never the Payment DB
Cohesion vs coupling — side by side
| Cohesion | Coupling | |
|---|---|---|
| About | inside one module | between modules |
| Good direction | high | low |
| Question it answers | "does this all belong together?" | "how much does A need to know about B?" |
| Fixed by | splitting a class/service by responsibility | depending on interfaces/contracts, not internals |
How This Connects
- Composition vs Inheritance — inheritance is tight coupling (child bound to the parent's internals); composition + interfaces keeps coupling loose.
- Dependency Injection — the main tool for lowering coupling: depend on an interface, get the implementation handed in.
- SOLID — high cohesion is the Single Responsibility Principle; low coupling is Dependency Inversion + Interface Segregation.
- Abstraction — an interface is the seam that lets two modules touch without knowing each other's details.
- Module 3 — HLD / Module 12 — Async — service boundaries, API contracts, and event-driven communication are all cohesion + coupling applied to architecture.
Trade-offs & common mistakes
- Don't build god classes/services because "it's all kind of related" — if the reasons to change are different, the responsibilities are different.
- Don't
newa concrete dependency inside a class — that's instant tight coupling. Inject the interface. - Don't let one service read another service's database — the tightest coupling there is.
- Note: you can over-split. A service per tiny operation adds network calls, deployment overhead, and distributed-systems problems. Split by domain boundary, not by method.
In the Interview
When to say it. Any "why did you split it this way?" or "why an interface / a queue / a gateway here?":
- "Payments and Orders are separate services — different responsibilities, different scaling needs, and they only talk through an API"
- "OrderService depends on a
PaymentServiceinterface, so the payment implementation can change without touching order logic"
The ~20-second version:
"I aim for high cohesion and low coupling. High cohesion means each class or service has one responsibility — a
UserServicethat also sends email and processes payments has three reasons to change, so I split it. Low coupling means modules depend on small contracts, not each other's internals —OrderServicedepends on aPaymentServiceinterface, not a concrete Razorpay class, and one service never reads another's database. At architecture scale that's exactly the microservices argument: cohesive services that communicate over APIs or events."
Follow-ups to expect:
- "How do you decide where to split a service?" — Along domain boundaries where the data and the reasons-to-change differ, not per operation.
- "How do interfaces reduce coupling?" — The caller depends on a method signature, not a class. The class behind it can be swapped, mocked, or rewritten with no impact on the caller.
- "What's the cost of low coupling done wrong?" — Over-splitting into nano-services: every call becomes a network hop with retries, partial failures, and versioning to manage.
Mistake to avoid: confusing "these things are related" with "these things belong in one class". Related domains still get their own cohesive modules that communicate through a boundary.
Quick Revision — 7 lines
- Cohesion = focus within one module. High = good (one responsibility).
- Coupling = dependency between modules. Low = good (know only a small contract).
- Good design = high cohesion + low coupling. The opposite is a god class.
- Lower coupling by depending on interfaces, not concrete classes, and injecting them.
- Microservices = cohesion at service level; "no shared DB" = low coupling.
- High cohesion = Single Responsibility Principle; low coupling = Dependency Inversion.
- You can over-split — split by domain boundary, not by method.