Dependency Inversion Principle (DIP)
Definition (Robert C. Martin):
1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend on abstractions.
In simple words: your important business code should talk to interfaces, not to concrete classes like SqlConnection, SmtpClient or a specific payment SDK.
- High-level module: code with business rules, for example
OrderService("place an order"). - Low-level module: technical details, for example "save in SQL Server", "send through SMTP", "call the Razorpay API".
- Abstraction: an interface or abstract class, for example
IOrderRepository,IEmailSender.
Real-life example
The plug on your phone charger doesn't care which power station made the electricity, and the power station doesn't care which phone you charge. Both depend on a standard socket (the abstraction). If your phone charger were wired directly into one power plant, changing anything would be impossible. That direct wiring is what new SqlOrderRepository() inside a business class does.
The problem: business code depends on details
public class SqlOrderRepository
{
public void Save(Order order) { /* ADO.NET INSERT into SQL Server */ }
}
public class SmtpEmailSender
{
public void Send(string to, string subject, string body) { /* SmtpClient */ }
}
public class OrderService // high-level business logic
{
private readonly SqlOrderRepository _repository = new SqlOrderRepository(); // tied to SQL Server
private readonly SmtpEmailSender _email = new SmtpEmailSender(); // tied to SMTP
public void PlaceOrder(Order order)
{
_repository.Save(order);
_email.Send(order.CustomerEmail, "Order placed", $"Order {order.Id} confirmed");
}
}
Dependency direction (bad):
OrderService ───► SqlOrderRepository ───► SQL Server
│
└────────────► SmtpEmailSender ───► SMTP server
Why is this a problem?
- Can't unit test: testing
PlaceOrderinserts a real row and sends a real email. - Can't change technology: moving to Cosmos DB, or from SMTP to SendGrid, means editing the business class.
- Ripple effect: a change in the low-level class (new constructor parameter) breaks the high-level class.
- Hidden dependencies: you can't see from outside what
OrderServiceneeds; it secretly creates them.
The solution: depend on abstractions
Step 1: The business layer owns the interfaces
public interface IOrderRepository
{
Task SaveAsync(Order order);
}
public interface IEmailSender
{
Task SendAsync(string to, string subject, string body);
}
Step 2: The business class depends only on the interfaces
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailSender _email;
// dependencies are given from outside (constructor injection)
public OrderService(IOrderRepository repository, IEmailSender email)
{
_repository = repository;
_email = email;
}
public async Task PlaceOrderAsync(Order order)
{
await _repository.SaveAsync(order);
await _email.SendAsync(order.CustomerEmail, "Order placed", $"Order {order.Id} confirmed");
}
}
Step 3: The details implement the interfaces
public class EfOrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public EfOrderRepository(AppDbContext db) => _db = db;
public async Task SaveAsync(Order order)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync();
}
}
public class SendGridEmailSender : IEmailSender
{
private readonly HttpClient _http;
public SendGridEmailSender(HttpClient http) => _http = http;
public async Task SendAsync(string to, string subject, string body)
{
// call the SendGrid REST API with _http
await Task.CompletedTask;
}
}
Dependency direction (good): the arrows now point TO the abstractions
OrderService ───► IOrderRepository ◄─── EfOrderRepository
│
└────────────► IEmailSender ◄─── SendGridEmailSender
This is the "inversion": the low-level classes now depend on interfaces defined for the business layer, instead of the business layer depending on them.
Step 4: Wire it up once, in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Shop")));
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddHttpClient<IEmailSender, SendGridEmailSender>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
Moving from SendGrid to Amazon SES tomorrow? Write SesEmailSender : IEmailSender and change one line in Program.cs. OrderService is not touched.
The biggest benefit: unit testing
Because OrderService depends on interfaces, a test can pass in fakes. Here we use xUnit and Moq:
public class OrderServiceTests
{
[Fact]
public async Task PlaceOrder_saves_order_and_sends_email()
{
// Arrange
var repository = new Mock<IOrderRepository>();
var email = new Mock<IEmailSender>();
var service = new OrderService(repository.Object, email.Object);
var order = new Order { Id = 101, CustomerEmail = "amit@example.com" };
// Act
await service.PlaceOrderAsync(order);
// Assert
repository.Verify(r => r.SaveAsync(order), Times.Once);
email.Verify(e => e.SendAsync("amit@example.com", "Order placed", It.IsAny<string>()), Times.Once);
}
}
No database, no email server, and the test runs in milliseconds. This is the main reason professional teams follow DIP.
DIP in Clean Architecture
Most enterprise .NET solutions are split into projects like this:
| Project | Contains | References |
|---|---|---|
| Shop.Domain | Entities: Order, Product | Nothing |
| Shop.Application | Business logic (OrderService) and interfaces (IOrderRepository, IEmailSender) | Domain |
| Shop.Infrastructure | Details: EfOrderRepository, SendGridEmailSender | Application |
| Shop.Api | Controllers and Program.cs (wires everything together) | Application, Infrastructure |
The key point: Application does not reference Infrastructure. The interfaces live with the business code, and Infrastructure implements them. That is exactly rule 2 of DIP: "details depend on abstractions".
DIP vs DI vs IoC
These three terms are often confused, and interviewers love asking about them.
| Term | What it is | Example |
|---|---|---|
| DIP: Dependency Inversion Principle | A design principle: depend on abstractions, not concretions | OrderService uses IOrderRepository |
| IoC: Inversion of Control | A general idea: the framework controls object creation and program flow, not your code | ASP.NET Core creates controllers and calls your actions |
| DI: Dependency Injection | A technique (one way to implement IoC and DIP): give dependencies from outside | Constructor injection |
| DI container | A tool that does DI automatically | builder.Services in ASP.NET Core |
You can follow DIP without a container (create objects manually in Main), and you can use a container without following DIP (registering only concrete classes). In ASP.NET Core we normally use both together. Read more in Dependency Injection in C# and Service Lifetime.
Common mistakes
- Interfaces that leak details:
IOrderRepository.SaveAsync(SqlCommand cmd)still ties callers to SQL Server. Abstractions should speak the business language (Order), not the technology. - Using
newinside business classes for services (entities and simple value objects likenew Order()are fine). - Service Locator: calling
serviceProvider.GetService<T>()everywhere hides dependencies. Prefer constructor injection. - An interface for everything: a
StringHelperor a DTO does not need an interface. Abstract things that talk to the outside world or that you want to swap or mock.
Summary
- High-level business code and low-level details should both depend on abstractions.
- The business layer owns the interfaces; infrastructure implements them.
- Use constructor injection and register implementations in
Program.cs. - Swapping a database, email provider or payment gateway becomes a one-line change.
- DIP makes unit testing with mocks possible.
- DIP is a principle, DI is a technique, IoC is the general idea, and the container is the tool.