random content
Have you ever fixed code in one place → only to trigger a chain of bugs somewhere else?
For example: you change the way data is stored, and suddenly all the logic in User, Product, and Order has to be modified too.
It feels like touching one tree root and the whole tree starts shaking 🌳.
The root cause is often that your code depends directly on implementation details instead of depending on abstractions (interfaces).
This is exactly where the Dependency Inversion Principle (DIP) comes in to save the day.
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
High-level modules: contain core business logic.
Low-level modules: contain technical details (database, API, file system, etc.).
Abstraction: a middle layer (interface, abstract class) that allows both sides to “talk” to each other.
Simply put:
instead of letting UserService tightly “hug” MySQL, let it depend on a Database interface instead.
MySQL, PostgreSQL, or MongoDB can stand behind that interface and do the actual work.
class MySQLDatabase {
save(data: string) {
console.log("Saving to MySQL:", data);
}
}
class UserService {
private db = new MySQLDatabase();
addUser(user: string) {
this.db.save(user);
}
}
What’s wrong here?
UserService depends directly on MySQLDatabase.
If you want to switch to PostgreSQL or Redis → you must immediately modify UserService.
Testing is also difficult because UserService is always tied to the real MySQLDatabase.
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string) {
console.log("Saving to MySQL:", data);
}
}
class PostgreSQLDatabase implements Database {
save(data: string) {
console.log("Saving to PostgreSQL:", data);
}
}
class UserService {
constructor(private db: Database) {}
addUser(user: string) {
this.db.save(user);
}
}
Now:
UserService doesn’t care whether it’s MySQL or PostgreSQL.
During testing, you can inject a MockDatabase to simulate saving behavior.
Changing the storage backend only requires adding a new class that implements Database, without touching existing code.
NestJS naturally applies DIP thanks to Dependency Injection (DI).
@Injectable()
class UserService {
constructor(private readonly db: Database) {}
addUser(user: string) {
this.db.save(user);
}
}
In AppModule, you simply bind the interface to an implementation:
@Module({
providers: [
UserService,
{ provide: 'Database', useClass: MySQLDatabase },
],
})
export class AppModule {}
Want to switch to PostgreSQL?
Just change useClass. Business logic remains untouched.
Easier maintenance: change technical details without affecting core logic.
Easier testing: inject mocks or fakes instead of real implementations.
Easier extensibility: add new databases, API providers, or storage systems without modifying old code.
Cleaner code: clear separation between business logic and technical details.
DIP is powerful, but if your project is:
Very small (e.g. a personal todo app).
Using only one database with no intention to change.
Then adding abstractions might unnecessarily complicate things.
Remember: principles are tools, not unbreakable laws.
The Dependency Inversion Principle helps your code become less “tightly coupled”, more flexible, and easier to maintain.
Think of abstractions as power sockets, and implementations as different plugs.
As long as you plug into the right socket, it doesn’t matter whether it’s an iron, a fridge, or a phone charger 🔌.
Next time you write a class, ask yourself:
Is it depending on an interface or a concrete class?
If I change the database or API tomorrow, will this code be easy to update?
If the answer is “yes, it’s easy”, congratulations — you’re on the right DIP path! 🎉