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, andOrderhas 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.
What is DIP?
Short definition:
High-level modules should not depend on low-level modules.
Both should depend on abstractions.
What does that mean?
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.
Bad example (violating DIP)
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?
UserServicedepends directly onMySQLDatabase.If you want to switch to PostgreSQL or Redis → you must immediately modify
UserService.Testing is also difficult because
UserServiceis always tied to the realMySQLDatabase.
Improving with DIP
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:
UserServicedoesn’t care whether it’s MySQL or PostgreSQL.During testing, you can inject a
MockDatabaseto simulate saving behavior.Changing the storage backend only requires adding a new class that implements
Database, without touching existing code.
DIP in NestJS
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.
Real-world benefits of applying DIP
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.
When NOT to be too strict
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.
Conclusion
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! 🎉