Dependency Inversion Principle (DIP)
I assume by DI here you mean the next SOLID principle: Dependency Inversion Principle.
Note that Dependency Inversion Principle (DIP) and Dependency Injection (DI) are related, but they are not the same thing.
1. What is Dependency Inversion Principle?
Dependency Inversion Principle (DIP) is the fifth principle of SOLID.
It states:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
And:
Abstractions should not depend on details. Details should depend on abstractions.
In simple English:
A class should depend on an interface/abstraction instead of being tightly connected to a specific concrete implementation.
For example, instead of:
OrderService
↓
EmailService
prefer:
OrderService
↓
IMessageService
↑
EmailService
Now OrderService knows about the contract, not the technical implementation.
2. Why Do We Need DIP?
Suppose we have an OrderService.
After successfully placing an order, it sends an email.
We directly create EmailService inside OrderService:
public class OrderService
{
private readonly EmailService _emailService;
public OrderService()
{
_emailService = new EmailService();
}
}
The dependency is:
OrderService
↓
EmailService
This is tight coupling.
OrderService directly knows:
"I must use EmailService."
3. What Problem Does Tight Coupling Cause?
Suppose today we use:
EmailService
Tomorrow the customer says:
Don't send Email. Send SMS.
Now we need:
SmsService
We have to modify OrderService.
Later:
Use WhatsApp.
Again modify OrderService.
Email
↓
Modify OrderService
SMS
↓
Modify OrderService
WhatsApp
↓
Modify OrderService
The high-level business logic is unnecessarily tied to communication technology.
DIP tries to remove this coupling.
4. High-Level and Low-Level Modules
This terminology is important for interviews.
High-Level Module
A high-level module normally contains business/application logic.
Examples:
OrderService
PaymentService
EmployeeService
CheckoutService
For our example:
OrderService
is the high-level module.
Its main concern is:
Process an order.
Low-Level Module
A low-level module contains technical implementation details.
Examples:
EmailService
SqlRepository
FileLogger
AzureBlobStorage
PaymentGateway
In our example:
EmailService
is a low-level implementation detail.
5. Without DIP
The dependency looks like:
High-Level Module
│
↓
Low-Level Module
OrderService
│
↓
EmailService
OrderService directly depends on EmailService.
This means:
OrderService knows exactly
how notification is implemented.
That's the coupling we want to reduce.
6. Before DIP — Bad Design
Here is a simple program:
using System;
public class EmailService
{
public void Send(string message)
{
Console.WriteLine(
$"Email sent: {message}");
}
}
public class OrderService
{
private readonly EmailService _emailService;
public OrderService()
{
_emailService = new EmailService();
}
public void PlaceOrder()
{
Console.WriteLine(
"Order placed successfully.");
_emailService.Send(
"Your order has been placed.");
}
}
public class Program
{
public static void Main()
{
OrderService orderService =
new OrderService();
orderService.PlaceOrder();
}
}
Output
Order placed successfully.
Email sent: Your order has been placed.
The program works.
But the design is tightly coupled.
7. Where is the Problem?
Look carefully:
private readonly EmailService _emailService;
OrderService directly depends on:
EmailService
And more importantly:
_emailService = new EmailService();
OrderService itself decides:
"I want EmailService."
So:
OrderService
│
│ directly creates
↓
EmailService
Now suppose we want:
SmsService
We must modify:
OrderService
That's what DIP helps us avoid.
8. Apply Dependency Inversion Principle
First create an abstraction:
public interface IMessageService
{
void Send(string message);
}
Then:
EmailService
implements:
IMessageService
Now OrderService depends on:
IMessageService
instead of:
EmailService
The design becomes:
IMessageService
↑ ↑
│ │
EmailService SmsService
↑
│
OrderService
depends on abstraction
More accurately, the source-code dependencies point toward the abstraction:
OrderService ──────→ IMessageService
EmailService ──────→ IMessageService
SmsService ──────→ IMessageService
9. Full Simple Program Using DIP
using System;
// Abstraction
public interface IMessageService
{
void Send(string message);
}
// Low-level implementation
public class EmailService : IMessageService
{
public void Send(string message)
{
Console.WriteLine(
$"Email sent: {message}");
}
}
// Another low-level implementation
public class SmsService : IMessageService
{
public void Send(string message)
{
Console.WriteLine(
$"SMS sent: {message}");
}
}
// High-level business service
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
Console.WriteLine(
"Order placed successfully.");
_messageService.Send(
"Your order has been placed.");
}
}
// Program
public class Program
{
public static void Main()
{
IMessageService messageService =
new EmailService();
OrderService orderService =
new OrderService(messageService);
orderService.PlaceOrder();
}
}
Output
Order placed successfully.
Email sent: Your order has been placed.
10. Program Explanation
Let's understand this carefully.
Step 1 — Create the Abstraction
public interface IMessageService
{
void Send(string message);
}
This interface defines the contract:
Any message service must provide
Send().
It doesn't say whether the message is sent through:
- SMS
- Push notification
It simply defines:
IMessageService
│
↓
Send()
11. EmailService Implements the Abstraction
public class EmailService : IMessageService
{
public void Send(string message)
{
Console.WriteLine(
$"Email sent: {message}");
}
}
Now:
EmailService
↓
implements
↓
IMessageService
EmailService contains the technical details for sending an email.
12. SmsService Also Implements the Abstraction
public class SmsService : IMessageService
{
public void Send(string message)
{
Console.WriteLine(
$"SMS sent: {message}");
}
}
Now we have:
IMessageService
↑ ↑
│ │
EmailService SmsService
Both satisfy the same contract.
13. Most Important Part — OrderService
Look at:
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
}
Notice:
private readonly IMessageService _messageService;
It does not say:
private readonly EmailService _messageService;
Therefore OrderService doesn't care whether the implementation is:
EmailService
SmsService
WhatsAppService
PushNotificationService
It only knows:
IMessageService
This is the core idea of DIP.
14. How is EmailService Passed?
In Main():
IMessageService messageService =
new EmailService();
Then:
OrderService orderService =
new OrderService(messageService);
So:
Program
│
↓
Creates EmailService
│
↓
Passes it to OrderService
│
↓
OrderService receives IMessageService
Inside the constructor:
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
The actual object is:
EmailService
but the variable is based on:
IMessageService
15. Complete Execution Flow
When this executes:
orderService.PlaceOrder();
we enter:
public void PlaceOrder()
{
Console.WriteLine(
"Order placed successfully.");
_messageService.Send(
"Your order has been placed.");
}
_messageService contains an EmailService object.
Therefore:
_messageService.Send(...)
calls:
EmailService.Send(...)
Flow:
Program
│
↓
new EmailService()
│
↓
OrderService(IMessageService)
│
↓
PlaceOrder()
│
↓
_messageService.Send()
│
↓
EmailService.Send()
│
↓
Email Sent
16. Now Change Email to SMS
This is where the benefit becomes clear.
Originally:
IMessageService messageService =
new EmailService();
Change it to:
IMessageService messageService =
new SmsService();
That's it.
The OrderService remains unchanged.
public class Program
{
public static void Main()
{
IMessageService messageService =
new SmsService();
OrderService orderService =
new OrderService(messageService);
orderService.PlaceOrder();
}
}
Output
Order placed successfully.
SMS sent: Your order has been placed.
17. What Happened?
Previously:
OrderService
↓
EmailService
Now:
OrderService
↓
IMessageService
↑
│
SmsService
OrderService doesn't change.
Only the implementation supplied to it changes.
18. Adding WhatsApp Later
Suppose tomorrow we need WhatsApp.
Create:
public class WhatsAppService : IMessageService
{
public void Send(string message)
{
Console.WriteLine(
$"WhatsApp sent: {message}");
}
}
Then:
IMessageService messageService =
new WhatsAppService();
Again:
OrderService
doesn't need to know the technical details of WhatsApp.
We now have:
IMessageService
↑ ↑ ↑
│ │ │
Email SMS WhatsApp
while:
OrderService
│
↓
IMessageService
remains stable.
19. Why is it Called "Dependency Inversion"?
Without DIP:
High-Level
↓
Low-Level
OrderService
↓
EmailService
The high-level business class depends directly on the low-level implementation.
With DIP:
OrderService
↓
IMessageService
↑
EmailService
Both sides are organized around an abstraction.
The high-level business logic no longer directly depends on the implementation detail.
That is the important inversion of dependency direction.
20. DIP vs Dependency Injection — Very Important
This is one of the most common interview questions.
Dependency Inversion Principle — DIP
A design principle.
It tells us:
Depend on abstractions rather than directly coupling high-level logic to low-level implementation details.
Example:
private readonly IMessageService _messageService;
Dependency Injection — DI
A technique for providing an object's dependencies from outside.
Example:
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
The dependency is supplied through the constructor.
So:
DIP
│
└── Design Principle
"Depend on abstraction"
DI
│
└── Technique
"Supply dependency from outside"
21. In Our Program, Where is DIP?
This is DIP:
private readonly IMessageService _messageService;
because OrderService depends on the abstraction.
22. Where is Dependency Injection?
This is Constructor Injection:
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
The dependency is passed from outside.
Therefore:
IMessageService
│
├── DIP → Depend on abstraction
│
└── DI → Dependency supplied externally
23. How ASP.NET Core Handles This
In our console example, we manually created:
IMessageService messageService =
new EmailService();
OrderService orderService =
new OrderService(messageService);
In ASP.NET Core, the built-in DI container normally handles object creation.
For example, registration:
builder.Services.AddScoped<IMessageService, EmailService>();
builder.Services.AddScoped<OrderService>();
Then your class:
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
public void PlaceOrder()
{
_messageService.Send(
"Order placed successfully.");
}
}
ASP.NET Core sees:
IMessageService messageService
and checks its DI registrations:
AddScoped<IMessageService, EmailService>();
It understands:
IMessageService requested
↓
Registration says
↓
Use EmailService
↓
Create EmailService
↓
Pass it to OrderService constructor
So you don't manually write:
new EmailService()
inside OrderService.
24. Very Important Clarification
You might think:
"ASP.NET Core has Dependency Injection, so DIP is automatically followed."
No.
You could register:
builder.Services.AddScoped<EmailService>();
and directly inject:
public OrderService(
EmailService emailService)
This uses Dependency Injection, because ASP.NET Core supplies EmailService.
But OrderService is still directly coupled to the concrete EmailService.
So:
Using DI
≠
Automatically following DIP
A common DIP-oriented design would be:
public OrderService(
IMessageService messageService)
with:
builder.Services
.AddScoped<IMessageService, EmailService>();
25. Before DIP vs After DIP
Before
OrderService
│
↓
EmailService
Problems:
- Tight coupling
- Harder to replace implementation
- Harder to isolate in unit tests
- Business logic knows technical implementation
After
IMessageService
↑ ↑
│ │
EmailService SmsService
↑
│
OrderService
depends on abstraction
Or from a dependency perspective:
OrderService ─────→ IMessageService
EmailService ─────→ IMessageService
SmsService ─────→ IMessageService
Benefits:
- Loose coupling
- Easier implementation replacement
- Easier testing
- Better extensibility
26. Real-Time Industry Examples
DIP is common in enterprise .NET applications.
Repository
Instead of:
EmployeeService
↓
SqlEmployeeRepository
use:
EmployeeService
↓
IEmployeeRepository
↑
SqlEmployeeRepository
OrderService
↓
IEmailService
↑
SendGridEmailService
File Storage
DocumentService
↓
IFileStorage
↑
AzureBlobStorage
Payment
CheckoutService
↓
IPaymentGateway
↑
StripePaymentGateway
The high-level application logic works against a stable contract rather than knowing all implementation details.
27. DIP in Clean Architecture
DIP is especially important in Clean Architecture.
Consider:
Web/API
│
↓
Application
│
↓
Domain
Infrastructure
Your Application layer might define:
IArticleRepository
while Infrastructure implements it:
ArticleRepository
Conceptually:
Application
│
├── IArticleRepository
│
↑
Infrastructure
│
└── ArticleRepository
This helps keep core business/application logic independent of technical infrastructure such as:
- EF Core
- SQL Server
- External APIs
- Email providers
- File storage
This is one of the most important practical uses of DIP in modern .NET architecture.
28. Advantages of Dependency Inversion Principle
1. Loose Coupling
High-level business logic is not tightly coupled to specific implementations.
2. Easy to Replace Implementations
For example:
EmailService
↓
SendGridService
or:
SQL Repository
↓
API Repository
with less impact on consumers.
3. Easier Unit Testing
You can replace real dependencies with:
Mock
Fake
Stub
during tests.
For example:
OrderService
↓
IMessageService
↑
FakeMessageService
No real email needs to be sent during a unit test.
4. Better Maintainability
Technical implementation changes have less impact on business logic.
5. Better Extensibility
New implementations can be added behind the same abstraction.
6. Works Well with Dependency Injection
ASP.NET Core's built-in DI container makes this style straightforward to implement.
7. Supports Clean Architecture
DIP helps keep the core layers independent from infrastructure details.
29. Common Mistake
Tight coupling
public class OrderService
{
private readonly EmailService _emailService
= new EmailService();
}
Here:
OrderService
↓
EmailService
Better abstraction-based design
public class OrderService
{
private readonly IMessageService _messageService;
public OrderService(
IMessageService messageService)
{
_messageService = messageService;
}
}
Now:
OrderService
↓
IMessageService
The concrete implementation can be selected externally.
30. Key Points
- DIP = Dependency Inversion Principle.
- It is the D in SOLID.
- High-level modules should not directly depend on low-level implementation details.
- Both should be organized around abstractions.
- In C#, abstractions are commonly represented using interfaces or abstract classes.
- DIP helps reduce tight coupling.
- Business logic should ideally not directly create infrastructure dependencies with
new. - DIP and Dependency Injection are different concepts.
- DIP is a design principle.
- DI is a technique for supplying dependencies.
- Constructor Injection is the most commonly used DI approach in ASP.NET Core.
- ASP.NET Core has a built-in Dependency Injection container; no third-party library is required for basic DI.
- Using ASP.NET Core DI does not automatically mean the application follows DIP.
- DIP improves testability, maintainability, extensibility, and flexibility.
- DIP is an important principle behind Clean Architecture.
Interview Answer
Dependency Inversion Principle states that high-level modules should not directly depend on low-level modules; both should depend on abstractions. In simple terms, business classes should depend on interfaces rather than concrete implementations. For example, an OrderService can depend on IMessageService instead of EmailService. EmailService, SmsService, or another implementation can then be supplied through Dependency Injection. This reduces tight coupling and makes the application easier to test, maintain, and extend.