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 Module → Low-Level Module
(Business Logic) → (Database, Email, Files)
DIP Approach:
High-Level Module → Abstraction ← Low-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:
- DIP is a design principle (what to do)
- Dependency Injection is a technique (how to do it)
- IoC Container is a tool (automates DI)
- High-level modules define abstractions, low-level implements them
- 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.