Summary
This article explores the Dependency Inversion Principle (DIP)—high-level modules should not depend on low-level modules; both should depend on abstractions. You'll learn how to break tight coupling through dependency injection, design flexible architectures, and make code testable and maintainable. Through practical C# examples, we'll transform rigid, tightly-coupled systems into loosely-coupled, extensible designs.
Prerequisites
Strong understanding of C# interfaces and abstract classes
Familiarity with dependency injection concepts
Knowledge of all previous SOLID principles recommended
Experience with IoC containers helpful but not required
The Dependency Inversion Principle states: High-level modules should not depend on low-level modules. Both should depend on abstractions .
Additionally: Abstractions should not depend on details. Details should depend on abstractions .
Let's unpack what this means and why it matters.
Understanding High-Level vs Low-Level
Before we dive into examples, let's clarify terminology:
High-level modules contain business logic and policies (e.g.,
OrderProcessor,PaymentService)Low-level modules handle implementation details (e.g.,
SqlDatabase,FileLogger,SmtpEmailer)
Traditionally, high-level modules depend directly on low-level modules. DIP inverts this: both depend on abstractions.
The Notification System Problem
You're building an order processing system:
public class OrderProcessor
{
private EmailService _emailService = new EmailService();
private SmsService _smsService = new SmsService();
public void ProcessOrder(Order order)
{
// Process the order
Console.WriteLine($"Processing order {order.Id}");
// Send notifications
_emailService.SendEmail(order.CustomerEmail, "Order Confirmed", "Your order has been confirmed");
_smsService.SendSms(order.CustomerPhone, "Order confirmed");
}
}
public class EmailService
{
public void SendEmail(string to, string subject, string body)
{
// SMTP logic
Console.WriteLine($"Sending email to {to}");
}
}
public class SmsService
{
public void SendSms(string phone, string message)
{
// SMS API logic
Console.WriteLine($"Sending SMS to {phone}");
}
}
Problems with this design:
OrderProcessor(high-level) directly depends onEmailServiceandSmsService(low-level)Can't test
OrderProcessorwithout actually sending emails and SMSCan't switch notification providers without modifying
OrderProcessorCan't add new notification types without changing
OrderProcessor
Inverting the Dependency
public interface INotificationService
{
void SendNotification(string recipient, string message);
}
public class EmailNotificationService : INotificationService
{
public void SendNotification(string recipient, string message)
{
Console.WriteLine($"Sending email to {recipient}: {message}");
// SMTP logic
}
}
public class SmsNotificationService : INotificationService
{
public void SendNotification(string recipient, string message)
{
Console.WriteLine($"Sending SMS to {recipient}: {message}");
// SMS API logic
}
}
public class PushNotificationService : INotificationService
{
public void SendNotification(string recipient, string message)
{
Console.WriteLine($"Sending push notification to {recipient}: {message}");
// Push notification logic
}
}
public class OrderProcessor
{
private readonly INotificationService _notificationService;
public OrderProcessor(INotificationService notificationService)
{
_notificationService = notificationService;
}
public void ProcessOrder(Order order)
{
Console.WriteLine($"Processing order {order.Id}");
_notificationService.SendNotification(order.CustomerEmail, "Order confirmed");
}
}
Now both OrderProcessor and the notification services depend on INotificationService . The dependency has been inverted.
Benefits:
Easy to test with mock notifications
Switch providers without changing
OrderProcessorAdd new notification types (Slack, WhatsApp) by implementing
INotificationService
The Data Access Layer Trap
Consider this tightly-coupled design:
public class CustomerService
{
private SqlCustomerRepository _repository = new SqlCustomerRepository();
public Customer GetCustomer(int id)
{
return _repository.GetById(id);
}
public void SaveCustomer(Customer customer)
{
_repository.Save(customer);
}
}
public class SqlCustomerRepository
{
public Customer GetById(int id)
{
using (var connection = new SqlConnection("connection-string"))
{
// SQL logic
return new Customer();
}
}
public void Save(Customer customer)
{
using (var connection = new SqlConnection("connection-string"))
{
// SQL logic
}
}
}
CustomerService is tightly coupled to SQL Server. Switching to MongoDB or a web API requires rewriting CustomerService .
Applying DIP
public interface ICustomerRepository
{
Customer GetById(int id);
void Save(Customer customer);
}
public class SqlCustomerRepository : ICustomerRepository
{
private readonly string _connectionString;
public SqlCustomerRepository(string connectionString)
{
_connectionString = connectionString;
}
public Customer GetById(int id)
{
using (var connection = new SqlConnection(_connectionString))
{
// SQL logic
return new Customer();
}
}
public void Save(Customer customer)
{
using (var connection = new SqlConnection(_connectionString))
{
// SQL logic
}
}
}
public class MongoCustomerRepository : ICustomerRepository
{
private readonly IMongoDatabase _database;
public MongoCustomerRepository(IMongoDatabase database)
{
_database = database;
}
public Customer GetById(int id)
{
var collection = _database.GetCollection("customers");
return collection.Find(c => c.Id == id).FirstOrDefault();
}
public void Save(Customer customer)
{
var collection = _database.GetCollection("customers");
collection.InsertOne(customer);
}
}
public class CustomerService
{
private readonly ICustomerRepository _repository;
public CustomerService(ICustomerRepository repository)
{
_repository = repository;
}
public Customer GetCustomer(int id)
{
return _repository.GetById(id);
}
public void SaveCustomer(Customer customer)
{
_repository.Save(customer);
}
}

Mariusz PostolPosted Nov 19, 2025, 10:45 AM
What is the difference between high-level modules and low-level modules? What is the module at all in terms of the programming language? You said, "Low-level modules handle implementation details" of the business logic?