Depend on Abstractions, Not on Concrete Implementations
The Dependency Inversion Principle (DIP), the fifth and final principle of SOLID, states that:
“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.” — Robert C. Martin
Let’s break this into two separate statements because each carries its own important meaning.
First Statement
High-level modules (those containing business logic) should not depend directly on low-level modules (those handling technical details such as database access or email delivery).
Instead, both layers should depend on abstractions, such as interfaces or abstract classes.
Second Statement
Abstractions themselves should not know about implementation details.
Instead, implementation details should conform to the contract defined by the abstraction.
Simply Put
Do not import concrete implementations where an interface is sufficient.
Real-Life Example: How to Understand It?
Imagine ordering food for home delivery.
You do not call a delivery driver directly. Instead, you call a restaurant and place an order.
The restaurant knows it needs someone capable of delivering the food to your address.
It does not matter which courier performs the delivery. What matters is that every courier can pick up the package and deliver it successfully.
Now imagine you insist that only one specific courier can deliver your food.
If that courier gets sick, quits the job, or is unavailable, you do not receive your meal.
You have become dependent on a specific person instead of the role that person fulfills.
Programming works the same way.
That “role” is an interface—an abstraction.
The restaurant (business logic) does not know anything about the individual courier (the concrete class).
It only knows that it needs someone who fulfills the role of a courier: picking up and delivering packages.
A Thought Exercise
Imagine an OrderProcessor class responsible for processing orders in an online store.
To complete an order, it must save data somewhere.
If OrderProcessor directly creates an instance of MySQLOrderRepository, then you are effectively calling the courier directly.
What happens when you decide to switch to PostgreSQL, MongoDB, or file-based storage for tests?
You must modify a class that should focus on business logic, not infrastructure details.
That is exactly the problem DIP solves.
Example — DIP Violation
Suppose we are building a notification system for an e-commerce platform.
The NotificationService class sends notifications to customers after they place an order.
Someone unfamiliar with DIP might write something like this:
“I need to send emails, so I’ll use
EmailSender.”
At first glance, the code appears reasonable.
However, let’s see what happens when requirements change—which they always do in real projects.
Scenario 1: Add SMS Notifications
The business wants to support SMS notifications in addition to email.
To achieve this, we must modify NotificationService, even though the business logic itself has not changed.
As a side effect, we are also moving toward violating SRP.
Scenario 2: Unit Testing
In unit tests, we do not want to send real emails.
Unfortunately, there is no easy way to replace EmailSender with a mock because the class creates it internally with new.
Scenario 3: Multiple Email Providers
Suppose we want to support:
- SendGrid,
- Mailgun,
- an internal SMTP server.
Every provider change requires modifying NotificationService, even though it should not care who actually sends emails.
Root Cause
NotificationService depends on a concrete implementation (EmailSender) instead of depending on an abstraction.
Example — DIP-Compliant Design
How do we fix this?
We introduce an abstraction: an interface that defines the contract:
“Anything capable of sending notifications can be used here.”
Now low-level implementations depend on the abstraction:
The high-level module also depends on the abstraction:
Usage
What Changed?
1. We Introduced an Interface
NotificationSender defines the contract.
Any notification sender must implement it.
2. We Removed Object Creation from Business Logic
NotificationService no longer creates dependencies using new.
Dependencies are provided externally.
This technique is called Dependency Injection (DI).
DIP is the principle. DI is a common technique used to implement it.
3. New Communication Channels Are Easy to Add
Want:
- push notifications,
- Slack messages,
- Microsoft Teams notifications,
- WhatsApp alerts?
Simply create another implementation of NotificationSender.
No changes are required in NotificationService.
4. Testing Becomes Easy
Tests can inject a fake sender instead of a real implementation.
Why Is DIP Important?
Flexibility and Replaceable Implementations
When business logic is isolated from technical details, changing:
- databases,
- notification channels,
- external APIs,
does not require changing the business logic itself.
It is like replacing a light bulb while keeping the same socket.
Better Testability
This is one of the biggest practical benefits.
When a class depends on an interface, tests can inject a mock object rather than using the real implementation.
No emails are sent.
No external systems are contacted.
The business logic is tested in isolation.
Fewer Team Conflicts
When dependencies are based on interfaces, different developers can work independently on:
NotificationService,EmailSender,SmsSender.
The interface acts as a contract between teams.
Long-Term Benefits
Large projects that ignore DIP often evolve into what engineers call:
“spaghetti dependencies”
A tangled network of concrete class dependencies.
Every modification creates a chain reaction.
Refactoring becomes dangerous.
Testing becomes difficult.
New developers may need weeks to understand the dependency graph.
DIP is an investment whose value grows alongside the size of the project.
When Should You Apply DIP?
When a Class Connects to External Resources
Examples include:
- databases,
- external APIs,
- file systems,
- payment gateways,
- messaging services.
The implementation may change, but business logic should remain unaffected.
When You Want Unit-Testable Code
If a class creates dependencies using:
new SomeService()
you cannot easily replace those dependencies during testing.
If dependencies are provided through constructors or setters, mocks become trivial to use.
In Frameworks That Already Use DIP
Spring Framework is essentially DIP at industrial scale.
Its IoC (Inversion of Control) container creates objects and injects dependencies automatically.
Annotations such as:
are mechanisms that support DIP.
OrderService depends on the PaymentGateway interface, not on a concrete implementation.
Spring decides which implementation to inject.
The class does not need to know.
Similarly, Hibernate and JPA use abstractions such as repositories and the EntityManager to separate business logic from database-specific details.
Memory Trick
Depend on the contract, not on the contractor.
If you see
new ConcreteService()inside a business class, the implementation decision is probably being made in the wrong place.
Point upward to abstractions, not downward to details.
Summary
DIP completes the SOLID principles.
- SRP says classes should have a single responsibility.
- OCP says software should be open for extension but closed for modification.
- LSP ensures subclasses can safely replace base classes.
- ISP promotes small, focused interfaces.
- DIP ties everything together by ensuring that high-level logic depends on stable abstractions rather than volatile implementation details.
Define a contract.
Depend on the contract.
Let implementation details serve the contract—not control it.

No responses yet