📄

Dependency Inversion Principle (DIP)

Intermediate 2 min read 300 words

High-level modules should not depend on low-level modules; both should depend on abstractions

Dependency Inversion Principle (DIP)

“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

The Dependency Inversion Principle inverts the traditional dependency direction. Instead of high-level code depending on low-level implementation details, both depend on abstractions (interfaces).

Understanding DIP

Traditional (Bad) Approach:

High-Level ModuleLow-Level Module
(Business Logic)(Database, Email, Files)

DIP Approach:

High-Level ModuleAbstractionLow-Level Module
(Business Logic)(Interface)(Database, Email, Files)

Violation Example

// BAD: High-level directly depends on low-level
public class EmailSender
{
    public void SendEmail(string to, string subject, string body)
    {
        using var client = new SmtpClient("smtp.gmail.com", 587);
        client.Credentials = new NetworkCredential("user@gmail.com", "password");
        client.EnableSsl = true;
        client.Send("from@gmail.com", to, subject, body);
    }
}

public class NotificationService
{
    // Direct dependency on concrete implementation
    private readonly EmailSender _emailSender = new EmailSender();

    public void NotifyUser(string email, string message)
    {
        _emailSender.SendEmail(email, "Notification", message);
    }
}

// Problems:
// 1. Can't test NotificationService without sending real emails
// 2. Changing email provider requires changing NotificationService
// 3. Can't use different notification methods (SMS, Push)
// 4. Tight coupling makes code rigid

Applying DIP

// 1. Define abstraction
public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

// 2. Low-level module implements abstraction
public class GmailSender : IEmailSender
{
    private readonly SmtpClient _client;

    public GmailSender(IOptions<GmailSettings> settings)
    {
        _client = new SmtpClient(settings.Value.Host, settings.Value.Port)
        {
            Credentials = new NetworkCredential(
                settings.Value.Username,
                settings.Value.Password),
            EnableSsl = true
        };
    }

    public async Task SendAsync(string to, string subject, string body)
    {
        var message = new MailMessage("from@gmail.com", to, subject, body);
        await _client.SendMailAsync(message);
    }
}

public class SendGridSender : IEmailSender
{
    private readonly SendGridClient _client;

    public SendGridSender(IOptions<SendGridSettings> settings)
    {
        _client = new SendGridClient(settings.Value.ApiKey);
    }

    public async Task SendAsync(string to, string subject, string body)
    {
        var msg = new SendGridMessage
        {
            From = new EmailAddress("from@example.com"),
            Subject = subject,
            PlainTextContent = body
        };
        msg.AddTo(new EmailAddress(to));
        await _client.SendEmailAsync(msg);
    }
}

// 3. High-level module depends on abstraction
public class NotificationService
{
    private readonly IEmailSender _emailSender;

    // Dependency injected
    public NotificationService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public async Task NotifyUserAsync(string email, string message)
    {
        await _emailSender.SendAsync(email, "Notification", message);
    }
}

// 4. Wire up in DI container
services.AddScoped<IEmailSender, SendGridSender>();  // Easy to swap!
services.AddScoped<NotificationService>();

DIP Enables Testing

// Now we can test NotificationService without real email
public class NotificationServiceTests
{
    [Fact]
    public async Task NotifyUser_SendsEmail()
    {
        // Arrange - Use mock
        var mockEmailSender = new Mock<IEmailSender>();
        var service = new NotificationService(mockEmailSender.Object);

        // Act
        await service.NotifyUserAsync("test@example.com", "Hello");

        // Assert - Verify without sending real email
        mockEmailSender.Verify(
            x => x.SendAsync("test@example.com", "Notification", "Hello"),
            Times.Once);
    }
}

DIP with Multiple Implementations

// Abstraction
public interface INotificationChannel
{
    string ChannelName { get; }
    Task SendAsync(string recipient, string message);
}

// Multiple implementations
public class EmailNotification : INotificationChannel
{
    public string ChannelName => "Email";
    public async Task SendAsync(string recipient, string message)
    {
        // Send email
    }
}

public class SmsNotification : INotificationChannel
{
    public string ChannelName => "SMS";
    public async Task SendAsync(string recipient, string message)
    {
        // Send SMS
    }
}

public class PushNotification : INotificationChannel
{
    public string ChannelName => "Push";
    public async Task SendAsync(string recipient, string message)
    {
        // Send push notification
    }
}

public class SlackNotification : INotificationChannel
{
    public string ChannelName => "Slack";
    public async Task SendAsync(string recipient, string message)
    {
        // Send Slack message
    }
}

// High-level service works with all channels
public class NotificationOrchestrator
{
    private readonly IEnumerable<INotificationChannel> _channels;
    private readonly ILogger<NotificationOrchestrator> _logger;

    public NotificationOrchestrator(
        IEnumerable<INotificationChannel> channels,
        ILogger<NotificationOrchestrator> logger)
    {
        _channels = channels;
        _logger = logger;
    }

    public async Task NotifyAsync(string recipient, string message, params string[] channelNames)
    {
        var selectedChannels = _channels
            .Where(c => channelNames.Contains(c.ChannelName, StringComparer.OrdinalIgnoreCase));

        foreach (var channel in selectedChannels)
        {
            try
            {
                await channel.SendAsync(recipient, message);
                _logger.LogInformation("Sent via {Channel}", channel.ChannelName);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to send via {Channel}", channel.ChannelName);
            }
        }
    }

    public async Task NotifyAllChannelsAsync(string recipient, string message)
    {
        var tasks = _channels.Select(c => c.SendAsync(recipient, message));
        await Task.WhenAll(tasks);
    }
}

// Register all implementations
services.AddScoped<INotificationChannel, EmailNotification>();
services.AddScoped<INotificationChannel, SmsNotification>();
services.AddScoped<INotificationChannel, PushNotification>();
services.AddScoped<INotificationChannel, SlackNotification>();
services.AddScoped<NotificationOrchestrator>();

DIP in Layered Architecture

// Domain Layer (highest level) - defines abstractions
namespace Domain.Interfaces
{
    public interface IOrderRepository
    {
        Task<Order> GetByIdAsync(int id);
        Task AddAsync(Order order);
    }

    public interface IPaymentService
    {
        Task<PaymentResult> ProcessAsync(PaymentDetails details);
    }
}

// Application Layer - uses abstractions
namespace Application.Services
{
    public class OrderService
    {
        private readonly IOrderRepository _orderRepository;
        private readonly IPaymentService _paymentService;

        public OrderService(
            IOrderRepository orderRepository,
            IPaymentService paymentService)
        {
            _orderRepository = orderRepository;
            _paymentService = paymentService;
        }

        public async Task<OrderResult> PlaceOrderAsync(OrderRequest request)
        {
            // Business logic using abstractions
            var payment = await _paymentService.ProcessAsync(request.PaymentDetails);
            if (!payment.Success)
                return OrderResult.Failed("Payment failed");

            var order = new Order(request);
            await _orderRepository.AddAsync(order);

            return OrderResult.Success(order.Id);
        }
    }
}

// Infrastructure Layer (lowest level) - implements abstractions
namespace Infrastructure.Data
{
    public class OrderRepository : IOrderRepository
    {
        private readonly DbContext _context;

        public OrderRepository(DbContext context)
        {
            _context = context;
        }

        public async Task<Order> GetByIdAsync(int id)
            => await _context.Orders.FindAsync(id);

        public async Task AddAsync(Order order)
        {
            await _context.Orders.AddAsync(order);
            await _context.SaveChangesAsync();
        }
    }
}

namespace Infrastructure.External
{
    public class StripePaymentService : IPaymentService
    {
        private readonly StripeClient _stripe;

        public StripePaymentService(StripeClient stripe)
        {
            _stripe = stripe;
        }

        public async Task<PaymentResult> ProcessAsync(PaymentDetails details)
        {
            // Stripe API calls
            return new PaymentResult { Success = true };
        }
    }
}

Dependency Injection Patterns

// Constructor Injection (Recommended)
public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
}

// Method Injection (For optional dependencies)
public class ReportGenerator
{
    public Report Generate(IDataSource dataSource)
    {
        var data = dataSource.GetData();
        return new Report(data);
    }
}

// Property Injection (Less common, use with caution)
public class LoggingService
{
    public ILogger Logger { get; set; } = NullLogger.Instance;

    public void DoWork()
    {
        Logger.Log("Working...");
    }
}

Benefits of DIP

Benefit Description
Loose Coupling Components don’t know about each other’s implementations
Testability Easy to mock dependencies
Flexibility Swap implementations without changing consumers
Parallel Development Teams can work on interfaces independently
Maintainability Changes isolated to specific implementations

Interview Tips

Common Questions:

  • “Explain DIP with an example”
  • “What’s the difference between DIP and Dependency Injection?”
  • “How does DIP relate to other SOLID principles?”

Key Points:

  1. DIP is a design principle (what to do)
  2. Dependency Injection is a technique (how to do it)
  3. IoC Container is a tool (automates DI)
  4. High-level modules define abstractions, low-level implements them
  5. Works together with OCP for extensibility

Common Mistake: DIP doesn’t mean “all classes need interfaces.” Create abstractions when:

  • You need multiple implementations
  • You want to test with mocks
  • You want to decouple modules
  • The implementation might change

Not every class needs an interface - simple data classes and utilities often don’t.