Tight coupling means that one part of your code depends heavily on another part.
If you change one part, you often have to change the other part too.
Simple example
class OrderService {
constructor() {
this.mysqlDatabase = new MySQLDatabase();
}
createOrder(order) {
this.mysqlDatabase.save(order);
}
}OrderService is tightly coupled to MySQLDatabase.
Why?
Because it directly creates and depends on a specific database implementation:
OrderService
↓
MySQLDatabaseIf you want to switch to PostgreSQL:
MySQL → PostgreSQLyou probably need to modify OrderService.
Loose coupling
Instead, you can depend on an abstraction:
class OrderService {
constructor(database) {
this.database = database;
}
createOrder(order) {
this.database.save(order);
}
}Now:
┌─ MySQLDatabase
OrderService ┤
└─ PostgreSQLDatabaseOrderService doesn’t care which database it receives.
Why tight coupling is usually bad
It makes code:
-
harder to change
-
harder to test
-
harder to reuse
-
harder to maintain
For example, testing this:
OrderService → MySQLDatabasemay require a real MySQL database.
With loose coupling, you can give it a fake:
OrderService → FakeDatabaseand test the service without a real database.
Mental model
Think of it like two objects being glued together:
Tight coupling: “I can’t change you without affecting me.”
Loose coupling: “I know what you can do, but I don’t care how you do it.”
Code smell connection: Tight coupling is often considered a design problem/code smell, especially when dependencies are concrete and spread throughout the application.