High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).
In practice, your business rule shouldn't know if you use MySQL, Stripe, or AWS. It should depend only on a contract (Interface) that says WHAT needs to be done, and not HOW it will be done.
RIGID IMPLEMENTATION
The most common mistake on a daily basis is calling a new ExternalTool() directly inside your use case.
When you do this, your code depends on the tool.
If the tool is discontinued or the API changes, you will have to open the "heart" of your system to fix it.
A BAD EXAMPLE
The business class is tightly coupled to the email provider SendGrid. This is a bad practice.
Code Example:
import { SendGridProvider } from 'sendgrid-sdk';
// BAD: The service instantiates the tool directly.
class RegisterUserUseCase {
private mailProvider: SendGridProvider;
constructor() {
// Tight coupling!
this.mailProvider = new SendGridProvider('API_KEY');
}
public async execute(email: string): Promise<void> {
// ...complex business logic...
// If SendGrid changes or goes down, this class breaks.
await this.mailProvider.sendEmail(email, "Welcome!");
}
}
TURNING THE TABLES
The solution is not to depend on the concrete class SendGridProvider, but rather to create a generic interface within our own domain.
Our service now receives this dependency ready via the constructor (Dependency Injection).
The most interesting part is that our application doesn't need to know exactly who is under the hood, whether it's Resend, EmailJS, or another, it just follows the template.
A GOOD EXAMPLE
The service now depends only on the contract. The infrastructure must obey.
Code Example:
// GOOD: The contract belongs to OUR application.
interface IMailProvider {
send(to: string, message: string): Promise<void>;
}
class RegisterUserUseCase {
// We receive the abstraction from the outside (Dependency Injection)
constructor(private mailProvider: IMailProvider) {}
public async execute(email: string): Promise<void> {
// ...complex business logic...
// The service has no idea which tool is sending it
await this.mailProvider.send(email, "Welcome!");
}
}
THE POWER OF INVERSION
The biggest advantage of DIP is Modularity.
If tomorrow your client asks to swap SendGrid for AWS SES, you just create a new implementation of the interface. The RegisterUserUseCase class suffers no changes.
Besides that, Unit Tests become absurdly easier, because you can inject a fake email Mock without needing to trigger real emails.
My Links
Github: victor-lis-bronzo
Linkedin: victor-lis-bronzo
Portfolio: portfolio.victorlisbronzo.me
Coolest Portfolio: victorlisbronzo.me
Leave your reaction ❤️
Were you already applying Dependency Injection before knowing it was the basis of DIP?