The SOLID design principles in C# are a set of fundamental object-oriented programming guidelines that help developers write clean, maintainable, and scalable code. SOLID is an acronym that represents five key principles: Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). These principles are widely used in .NET development to create robust software architectures that are easier to test, extend, and maintain.
Introduction to the Single Responsibility Principle (SRP) in .NET
In this article, I will explore SRP using a real-world e-commerce order processing example:
- Identify the problems that occur when the Single Responsibility Principle (SRP) is violated
- Refactor the code step-by-step to correctly implement SRP.
- Experience how breaking responsibilities into dedicated components results in cleaner, more testable, and easier-to-maintain applications.
By the end, you’ll have a clear understanding of how to implement SRP in .NET and write professional, future-ready code.
What is the Single Responsibility Principle?
- A class should do one job and have only one reason to change.
- Focus on a single, well-defined responsibility.
While the Single Responsibility Principle (SRP) may sound simple in theory, it is one of the most frequently broken rules in real-world .NET development. The challenge often arises in large and complex projects, such as e-commerce platforms, enterprise-level applications, or API-driven systems where developers may be tempted to place multiple responsibilities into a single class for convenience or speed.
Over time, this shortcut leads to tightly coupled code, making applications harder to maintain, test, and scale. For example, in an e-commerce system, a single class might handle order processing, validate order, database persistence, and email notifications all together, which violates SRP. Any change in one responsibility, like updating the database schema or modifying the email template, forces you to touch unrelated code, increasing the risk of bugs and regressions.
Let’s consider an example from an e-commerce application to examine how the Single Responsibility Principle (SRP) can be violated and then corrected through its implementation.
Understanding SRP with a Real-World E-Commerce Example
❌ SRP Violated: Bad Example in E-Commerce
Imagine a class in an online store that is responsible for all aspects of order processing, including validating order inputs, saving the order details to the database, and sending a confirmation email once the order is confirmed.
public class OrderService
{
public void PlaceOrder()
{
// Validate order using this methos
ValidateOrder();
// Save order to database
SaveOrderToDatabase();
// send eamil to user after order confirmation
SendConfirmationEmail();
}
private void ValidateOrder()
{
// Logic to validate order
}
private void SaveOrderToDatabase()
{
// Logic to save order to the database
Console.WriteLine("Order saved to database.");
}
private void SendConfirmationEmail()
{
// Logic to send order confirmation email
Console.WriteLine("Order confirmation email sent.");
}
}
Why This Violates SRP
- Order processing (business logic)
- Validate order (validation logic)
- Database persistence (data access logic)
- Email notification (communication logic)
All these are mixed in one class, so:
- If any change in validation → we modify OrderService.
- If database logic changes → we modify OrderService.
- If email format changes → we modify OrderService.
- If order logic changes → we modify OrderService.
When the application grows in complexity, this leads to the following challenges:
- Tight coupling between unrelated responsibilities
- Difficult to test each responsibility in isolation
- Code duplication if other services need similar functionality
This design makes the code harder to maintain, test, and scale as the application grows.
Applying SRP in .NET Step-by-Step
✅ Refactored Example: Applying the Single Responsibility Principle (SRP)
In this example, we’ll implement the Single Responsibility Principle (SRP) by assigning a single, well-defined responsibility to each class. We’ll define separate interfaces for these responsibilities, implement them in dedicated classes, and inject them into the OrderService using dependency injection.
Refactoring Code to Follow SRP: Step-by-Step
We created separate interfaces for each responsibility: validation, data persistence, and email notification to promote the Single Responsibility Principle:
- Validation Interface
- Persistence Interface
- Notification Interface
We implemented each interface in its respective class:
- OrderValidator: implements the validation logic.
- OrderRepository: handles database operations.
- EmailService: manages email notifications.
Finally, we injected all these components into the OrderService using constructor-based dependency injection, achieving a loosely coupled, modular, and highly testable architecture.
// 1. Interface for Validation
public interface IOrderValidator
{
void Validate();
}
// 2. Interface for Persistence
public interface IOrderRepository
{
void Save();
}
// 3. Interface for Notification
public interface IEmailService
{
void SendConfirmationEmail();
}
// 4. Implementation of OrderValidator
public class OrderValidator : IOrderValidator
{
public void Validate()
{
// Validation logic
Console.WriteLine("Order validated.");
}
}
// 5. Implementation of OrderRepository
public class OrderRepository : IOrderRepository
{
public void Save()
{
// Save logic
Console.WriteLine("Order saved to database.");
}
}
// 6. Implementation of EmailService
public class EmailService : IEmailService
{
public void SendConfirmationEmail()
{
// Email logic
Console.WriteLine("Order confirmation email sent.");
}
}
// 7. Final SRP + DI-Compliant OrderService
public class OrderService
{
private readonly IOrderValidator _validator;
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
public OrderService(IOrderValidator validator, IOrderRepository repository, IEmailService emailService)
{
_validator = validator;
_repository = repository;
_emailService = emailService;
}
public void PlaceOrder()
{
_validator.Validate();
_repository.Save();
_emailService.SendConfirmationEmail();
}
}
✅ How to Use with Dependency Injection Container (e.g., in ASP.NET Core)
In your Startup.cs or Program.cs:
services.AddScoped<IOrderValidator, OrderValidator>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<OrderService>();
Benefits of refactored code example
SRP: Each class has one responsibility.
DIP: OrderService depends on abstractions, not concrete implementations.
DI-ready: Clean, testable, and maintainable.
Summary:
In this article, we explored how developers can violate the Single Responsibility Principle (SRP) and how to refactor code to fully comply with it.
We started with a bad code example from an e-commerce order processing scenario, where one class handled order validation, saved orders to the database, and sent email confirmations. Combining these responsibilities in a single class clearly broke SRP.
Next, we refactored the code by creating separate interfaces for each responsibility, implementing them in dedicated classes, and integrating them into the OrderService through dependency injection. This refactoring gave each class a single, well-defined responsibility, producing cleaner, more maintainable, and SRP-compliant code.
Check out my other posts 
- Code Review Checklist for Clean, Secure, and Maintainable Code
- .NET Coding Best Practices Every Developer Should Follow
- LINQ Methods in .NET 9 and C# 13 : Index, CountBy, and AggregateBy
- Best Practices for Naming Boolean Variables in C# for Cleaner Code
- Any() vs Count() in LINQ – Best Practices for Performance and Efficiency