Dependency Inversion Principle (DIP)
Status: Complete
Category: SOLID Principles
Default enforcement: Advisory
Author: PushBackLog team
Tags
- Topic: quality, architecture
- Methodology: SOLID
- Skillset: any
- Technology: generic
- Stage: execution, review
Summary
High-level modules should not depend on low-level modules — both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. Concrete implementations are injected, not hard-coded.
Rationale
The dependency problem without inversion
In a naive system, high-level policy modules import low-level mechanism modules directly:
OrderService → MySQLOrderRepository
OrderService → StripeClient
OrderService → SmtpEmailSender
OrderService — the most business-valuable code — is shackled to specific databases, payment providers, and email implementations. Changing any one of those forces changes in OrderService. Testing OrderService in isolation requires a real database, a real payment gateway, or mocking at the file-system level. The most important code in the system is the hardest to test and the most entangled to change.
Inversion of control
DIP is the principle underlying dependency injection and inversion of control. Rather than high-level modules pulling in their dependencies, they declare what they need via an abstraction, and a higher-level assembler (a DI container, a factory, or just a constructor parameter) pushes in the concrete implementation.
The dependency arrow is inverted: both OrderService and MySQLOrderRepository now depend on an IOrderRepository interface. The interface is owned by, or co-located with, the policy layer — not the infrastructure layer.
Testability as a first principle
DIP is one of the most powerful testability enablers. When OrderService depends on IPaymentGateway, tests can inject a FakePaymentGateway or a StubPaymentGateway that returns controlled responses. The entire business logic can be exercised in milliseconds, with no network, no database, and no flakiness.
Guidance
The dependency injection pattern
The most common way to apply DIP is constructor injection: declare your dependencies as interface parameters in the constructor, and let the caller (or DI container) provide the implementation.
// Without DIP
class OrderService {
private db = new MySQLOrderRepository(); // hard-coded detail
private mailer = new SmtpEmailSender(); // hard-coded detail
}
// With DIP
class OrderService {
constructor(
private repository: IOrderRepository, // abstraction
private mailer: IEmailSender // abstraction
) {}
}
Interface ownership
A subtlety: the interface should be owned by the consumer (the high-level policy), not the producer (the low-level mechanism). If IOrderRepository lives in the infrastructure package alongside MySQLOrderRepository, OrderService still depends transitively on infrastructure — the arrow isn’t truly inverted. Place interfaces in the domain or application layer.
DI containers vs manual injection
| Approach | When to use |
|---|---|
| Manual construction (main function, factory) | Small systems, explicit wiring easier to understand |
| DI container (Spring, NestJS, .NET Core IoC) | Medium–large systems with many dependencies; reduces boilerplate |
| Partial manual + factory | Tests use manual injection; production uses container |
DI containers are powerful but add a layer of indirection. Ensure the team understands what the container is doing.
Examples
User notification service
// Abstraction (lives in application layer)
interface INotificationSender {
send(userId: string, message: string): Promise<void>;
}
// High-level policy — no knowledge of how notifications are sent
class UserEventService {
constructor(private sender: INotificationSender) {}
async onUserRegistered(userId: string) {
await this.sender.send(userId, 'Welcome to PushBackLog!');
}
}
// Low-level implementations — details depend on the abstraction
class EmailSender implements INotificationSender { ... }
class SmsSender implements INotificationSender { ... }
class FakeSender implements INotificationSender {
sent: { userId: string; message: string }[] = [];
async send(userId: string, message: string) {
this.sent.push({ userId, message });
}
}
// In tests
const fakeSender = new FakeSender();
const service = new UserEventService(fakeSender);
await service.onUserRegistered('user-123');
assert(fakeSender.sent.length === 1); // No SMTP server needed
Anti-patterns
1. new inside business logic
new Database(), new EmailClient(), or new HttpClient() inside a domain class hard-codes the dependency. Extract to a constructor parameter.
2. Service locator pattern
const db = ServiceLocator.get<IDatabase>('database');
Service locator is sometimes presented as DIP-compliant because it uses an interface, but it hides dependencies and makes them invisible at the constructor boundary. It’s harder to test and harder to reason about.
3. Injecting containers into classes
constructor(private container: Container) {
this.db = container.get(IDatabase);
}
This re-introduces the service locator pattern via DI. Inject the resolved dependency itself, not the container.
4. Abstractions that track details
An IOrderRepository interface with a getRawSql() method, or a IPaymentGateway with getStripeCustomerId(), leaks implementation details into the abstraction, defeating the purpose of the interface.
Related practices
Part of the PushBackLog Best Practices Library. Suggest improvements →