← Back to Article List         
Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP)

Published on 28 Sep 2026     11 min read SOLID Principles
SOLID Principles

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:

  • Email
  • SMS
  • WhatsApp
  • 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

Email

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.