If SOLID principles are the rules, coupling and cohesion are the metrics that tell us if we’re doing it right. Every good design aims for the same thing: low coupling, high cohesion.
What is Coupling?
Coupling is how much one class depends on another class. The more a class knows about another class’s internals, the tighter the coupling.
Think of it like people in an office. If Alice can’t do her job without knowing exactly how Bob organizes his desk, that’s tight coupling. If Alice just emails Bob a request and gets a response back — that’s loose coupling.
Tight Coupling (Bad)
# BAD -- OrderService knows the INTERNAL structure of Database
class MySQLDatabase:
def __init__(self):
self.connection = "mysql://localhost:3306"
def execute_query(self, sql):
print(f"Running SQL: {sql}")
class OrderService:
def __init__(self):
self.db = MySQLDatabase() # directly creates the dependency
def create_order(self, item):
# knows it's MySQL, knows the SQL syntax, knows the table name
self.db.execute_query(f"INSERT INTO orders VALUES ('{item}')")
// BAD -- OrderService is glued to MySQLDatabase
class MySQLDatabase {
constructor() {
this.connection = "mysql://localhost:3306";
}
executeQuery(sql) {
console.log(`Running SQL: ${sql}`);
}
}
class OrderService {
constructor() {
this.db = new MySQLDatabase(); // hardcoded
}
createOrder(item) {
this.db.executeQuery(`INSERT INTO orders VALUES ('${item}')`);
}
}
// BAD -- tightly coupled to MySQL
class MySQLDatabase {
void executeQuery(String sql) {
System.out.println("Running SQL: " + sql);
}
}
class OrderService {
private MySQLDatabase db = new MySQLDatabase(); // hardcoded
void createOrder(String item) {
db.executeQuery("INSERT INTO orders VALUES ('" + item + "')");
}
}
Want to switch to PostgreSQL? We have to rewrite OrderService. Want to test without a real database? Can’t.
Loose Coupling (Good)
from abc import ABC, abstractmethod
# GOOD -- OrderService depends on an abstraction
class Database(ABC):
@abstractmethod
def save(self, table, data):
pass
class MySQLDatabase(Database):
def save(self, table, data):
print(f"MySQL: saving to {table}")
class PostgresDatabase(Database):
def save(self, table, data):
print(f"Postgres: saving to {table}")
class OrderService:
def __init__(self, db: Database): # accepts ANY Database
self.db = db
def create_order(self, item):
self.db.save("orders", {"item": item})
# Easy to swap
service = OrderService(PostgresDatabase())
service.create_order("Laptop")
// GOOD -- OrderService takes any object with a save() method
class MySQLDatabase {
save(table, data) {
console.log(`MySQL: saving to ${table}`);
}
}
class PostgresDatabase {
save(table, data) {
console.log(`Postgres: saving to ${table}`);
}
}
class OrderService {
constructor(db) {
this.db = db; // injected -- could be anything
}
createOrder(item) {
this.db.save("orders", { item });
}
}
const service = new OrderService(new PostgresDatabase());
service.createOrder("Laptop");
interface Database {
void save(String table, Object data);
}
class MySQLDatabase implements Database {
public void save(String table, Object data) {
System.out.println("MySQL: saving to " + table);
}
}
class PostgresDatabase implements Database {
public void save(String table, Object data) {
System.out.println("Postgres: saving to " + table);
}
}
class OrderService {
private Database db; // depends on interface
OrderService(Database db) { this.db = db; }
void createOrder(String item) {
db.save("orders", item);
}
}
Now OrderService has no idea if it’s talking to MySQL, Postgres, or a mock. That’s loose coupling.
What is Cohesion?
Cohesion is how related the responsibilities within a single class are. A class with high cohesion does one thing well. A class with low cohesion is a grab bag of unrelated stuff.
Think of it like a toolbox. A well-organized toolbox (high cohesion) has screwdrivers in one section, wrenches in another. A junk drawer (low cohesion) has batteries, tape, a fork, and a birthday candle.
Low Cohesion (Bad)
# BAD -- this class does too many unrelated things
class Utils:
def send_email(self, to, message):
print(f"Email to {to}")
def calculate_tax(self, amount):
return amount * 0.18
def resize_image(self, image, width):
print(f"Resizing to {width}px")
def generate_report(self, data):
print("Generating report...")
Email, tax, images, and reports have nothing in common. This is the classic “Utils” or “Helper” class anti-pattern.
High Cohesion (Good)
# GOOD -- each class has a focused, related set of methods
class EmailService:
def send(self, to, message):
print(f"Email to {to}")
def send_bulk(self, recipients, message):
for r in recipients:
self.send(r, message)
class TaxCalculator:
def calculate(self, amount):
return amount * 0.18
def calculate_with_cess(self, amount):
return amount * 0.18 + amount * 0.01
Every method in EmailService is about emails. Every method in TaxCalculator is about taxes. High cohesion.
The Sweet Spot
How to Measure (Quick Gut Check)
Coupling — ask: “If I change class A, how many other classes break?”
- If the answer is “many” — coupling is too high
- If the answer is “none or few” — we’re in good shape
Cohesion — ask: “Can I describe what this class does in ONE sentence without using the word ‘and’?”
- “This class manages user authentication” — high cohesion
- “This class sends emails AND calculates taxes AND resizes images” — low cohesion
Connection to SOLID
Low coupling and high cohesion aren’t separate ideas from SOLID — they’re the RESULT of following SOLID:
- SRP directly increases cohesion (one responsibility per class)
- DIP directly reduces coupling (depend on abstractions)
- ISP increases cohesion of interfaces (small, focused contracts)
- OCP reduces coupling to specific implementations (extend, don’t modify)
In LLD interviews, every time we split a god class into focused services or introduce an interface between two classes, we’re improving coupling and cohesion. The interviewer might not name these terms, but they’ll notice the quality of our design.