Skip to content

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 PlaceOrder inserts 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 OrderService needs; 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:

ProjectContainsReferences
Shop.DomainEntities: Order, ProductNothing
Shop.ApplicationBusiness logic (OrderService) and interfaces (IOrderRepository, IEmailSender)Domain
Shop.InfrastructureDetails: EfOrderRepository, SendGridEmailSenderApplication
Shop.ApiControllers 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.

TermWhat it isExample
DIP: Dependency Inversion PrincipleA design principle: depend on abstractions, not concretionsOrderService uses IOrderRepository
IoC: Inversion of ControlA general idea: the framework controls object creation and program flow, not your codeASP.NET Core creates controllers and calls your actions
DI: Dependency InjectionA technique (one way to implement IoC and DIP): give dependencies from outsideConstructor injection
DI containerA tool that does DI automaticallybuilder.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 new inside business classes for services (entities and simple value objects like new Order() are fine).
  • Service Locator: calling serviceProvider.GetService<T>() everywhere hides dependencies. Prefer constructor injection.
  • An interface for everything: a StringHelper or 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.