The Single Responsibility Principle (SRP) is one of the five essential SOLID design principles that guide object-oriented programming in C# and .NET. It is usually the first principle developers learn, and the one they argue about the most. These principles are guidelines for the proper usage of object-oriented features, and the SRP definition is short:
A class should have a single responsibility, and this responsibility should be entirely encapsulated by the class.
That definition raises an obvious question: what is a responsibility in software design? There is no trivial answer, which is why Robert C. Martin (Uncle Bob) rewrote the SRP with a sentence that has become its canonical one-liner:
A class should have one, and only one, reason to change.
And that raises the next question: what is a reason to change?
The SRP is a principle you cannot easily infer from its definition alone. It also leaves plenty of room for opinions and interpretation. So what is the SRP actually about? SRP is about logic partitioning in your C# code: deciding which logic should be declared in which class so that each class stays focused, cohesive, and easy to change.
The goal of this post is to propose objective and concrete guidelines to increase your classes’ compliance with the SRP. In fine, this increases the maintainability of your .NET code.
The SOLID Principles in C#: A Quick Summary
Before delving into the Single Responsibility Principle, let’s see where it stands among the five SOLID design principles:
- The Single Responsibility Principle (SRP): A class should have one reason to change. This principle is about how to partition your logic into classes and avoid ending up with monster classes (known as a god class).
- The Open-Close Principle (OCP): Modules should be open for extension and closed for modification. To implement a new feature, it is better to add a new derived class instead of modifying existing code.
- The Liskov Substitution Principle (LSP): Methods that use references to base classes must be able to use objects of derived classes without knowing it. Array implements IList<T> but throws NotSupportException on IList<T>.Add(). This is a clear LSP violation.
- The Interface Segregation Principle (ISP): The client should not depend by design on methods it does not use. This is why designers introduced interfaces like IReadOnlyCollection<T>. Often the client just needs a subset of features, like read-only access instead of full read-write access.
- The Dependency Inversion Principle (DIP): Depend on abstractions, not on implementations. Interfaces are much less subject to change than classes, especially the ones that abide by the (ISP) and the (LSP).
Something worth keeping in mind is that SRP is the only SOLID principle not related to the usage of abstraction and polymorphism. It is purely about where you place your code.
Single Responsibility Principle Explained with a C# Example
The fastest way to understand the SRP in C# is to look at a class that breaks it. Let’s use the ActiveRecord pattern to show a typical SRP violation and how to fix it. An ActiveRecord is a class with two well-identified responsibilities:
- First, an ActiveRecord object stores in-memory data retrieved from a relational database.
- Second, the record is active. It is active in the sense that it mirrors data in memory and data in the relational database. For that, the CRUD (Create Read Update Delete) operations are implemented by the ActiveRecord itself.
To make things concrete, an ActiveRecord class can look like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Employee { public string Name { get; set; } public string Address { get; set; } ... public static Employee LoadFromName(string name) { //below query will get only a value from DB string sqlQuery = "SELECT * FROM Employee WHERE Name = '" + name + "' LIMIT 1;"; // get value from DB and fill this employee with obtained value Employee record = new Employee() { Name = name, Address = ...}; return record; } public bool Create() { // create the record in DB } public bool Update() { // update the record in DB ... } public bool Delete() { // delete the record in DB ... } } |
This Employee class has two reasons to change: the shape of the employee data, and the way employees are persisted. That is one responsibility too many.
If the class Employee didn’t know about persistence, it would be a Plain Old CLR Object, what we call a POCO class. A class is POCO if it depends only on C# primitive types (int, string, bool…) and on other POCO types. Being a POCO class is a good start to abide by the SRP, because it keeps the class away from concerns like Databases, User Interfaces, Networks, or Threading.
Consequently, if the Employee class is a POCO, it implies that a dedicated module (a repository, an ORM context, a data access layer…) manages the persistence.
Benefits of Applying the Single Responsibility Principle
Splitting the data from its persistence in C# pays off quickly:
- Not all client code of the class Employee wants to deal with persistence.
- More importantly, an Employee consumer really needs to know when an expensive DB roundtrip is triggered. If the Employee class is responsible for persistence, who knows whether the data is persisted as soon as a setter is invoked?
So it is better to isolate the persistence layer accesses and make them explicit. This is why at NDepend we promote rules like UI layer shouldn’t use directly DB types, which can be easily adapted to enforce any sort of code isolation.
Single Responsibility Principle and Cross-Cutting Concerns
Persistence is what we call a cross-cutting concern, an aspect of the implementation that tends to spread all over the code. We can expect most domain objects to be concerned with persistence. Other cross-cutting concerns we want to keep separate from domain objects include validation, logging, authentication, error handling, threading, networking, user interface and caching. Separating domain entities from these cross-cutting concerns is the very heart of separation of concerns, and it is achievable through OOP patterns like the decorator pattern. Alternatively, some Object-Relational Mapping (ORM) frameworks and some Aspect-Oriented Programming (AOP) frameworks can be used.
Single Responsibility Principle and the Reason to Change
Let’s consider this second version of Employee:
|
1 2 3 4 5 6 7 |
public class Employee { public string Name { get; set; } public string Address { get; set; } ... public void ComputePay() { ... } public void ReportHours() { ...} } |
The ComputePay() behavior is under the responsibility of the finance people, and the ReportHours() behavior is under the responsibility of the operational people. So if a finance person needs a change in ComputePay(), we can assume this change won’t affect the ReportHours() method. According to the version of SRP that states “a class should have one reason to change”, it is wise to declare these methods in different, dedicated modules. As a result, a change in ComputePay() has no risk of affecting the behavior of ReportHours(), and vice versa. In other words, we want these two parts of the code to be independent because they evolve independently and for different people.
This is why Robert C. Martin wrote that SRP is about people: make sure that logic controlled by different people is implemented in different modules. When you struggle to find the “reason to change”, ask who would request the change. Different actors mean different responsibilities.
Single Responsibility Principle and High Cohesion (LCOM)
The SRP is about encapsulating logic and data in a class because they fit well together. Fit well means the class is cohesive, in the sense that most methods use most fields. You can actually measure the cohesion of a class with the Lack of Cohesion Of Methods (LCOM) metric. See below an explanation of LCOM (extracted from this great Stuart Celarier placemat). What matters is to understand that if all methods of a class use all instance fields, the class is considered utterly cohesive and gets the best LCOM score, which is 0 or close to 0.
Typically, the effect of an SRP violation is to partition the methods and fields of a class into groups with few connections between them. The fields needed to compute the pay of an employee are not the same as the fields needed to report pending work. This is why we can estimate adherence to the SRP, and take action, based on the LCOM metric. You can use the rule Avoid types with poor cohesion to track classes with poor cohesion between methods and fields.
Single Responsibility Principle and Fat Code Smells
We can hardly find an easy definition of what a responsibility is. However, we noticed that adhering to the SRP usually results in classes with a good LCOM metric score.
On the other hand, not adhering to the SRP usually leads to the god class phenomenon. A god class knows too much and does too much, and is usually too large. Violations of rules like Avoid types too big, Avoid types with too many methods and Avoid types with too many fields are good candidates to spot god classes. Once a god class is identified, it can be refactored into a finer-grained design that respects the SRP.
Simple Guidelines to Adhere to the Single Responsibility Principle in C#
Here are objective and concrete guidelines to adhere to the SRP in your C# and .NET code:
- Keep domain classes isolated from cross-cutting concerns: code responsible for persistence, validation, logging, authentication, error handling, threading, caching…
- When implementing your domain, favor POCO classes that have no dependency on an external framework. A POCO class is not necessarily a fields-and-properties-only class; it can implement logic and behavior related to its own data.
- Use your understanding of the domain to partition code: logic related to different business functions should be kept separate to avoid interference.
- Regularly check the Lack of Cohesion Of Methods (LCOM) score of your classes.
- Regularly check for classes that are too large and too complex.
Single Responsibility Principle FAQ
What is the Single Responsibility Principle in C#?
The Single Responsibility Principle (SRP) states that a C# class should have one, and only one, reason to change. In practice it means each class should be responsible for a single part of the software’s functionality, and that responsibility should be fully encapsulated by the class. It is the first of the five SOLID principles.
What is a simple example of the SRP in C#?
A classic SRP violation is an ActiveRecord-style Employee class that both holds employee data and runs its own SQL to create, read, update and delete itself. That class has two reasons to change: the employee data model and the persistence mechanism. The SRP-compliant version keeps Employee as a POCO and moves persistence into a dedicated repository or data access layer.
What does “one reason to change” mean?
A “reason to change” is tied to the people or business function that requests a change. If finance drives changes to ComputePay() and operations drives changes to ReportHours(), those are two separate reasons to change, so they belong in two separate classes. When two responsibilities live in the same class, a change requested by one group can accidentally break the other.
How do you measure whether a C# class respects the SRP?
There is no single perfect metric, but the Lack of Cohesion Of Methods (LCOM) score is a strong indicator: a cohesive class where most methods use most fields scores close to 0. Class-size metrics also help, since classes that are too big or have too many methods and fields are typical god classes that violate the SRP. Tools like NDepend can flag these classes automatically.
What is the difference between the SRP and separation of concerns?
Separation of concerns is the broad idea that different concerns, such as persistence, UI, and business logic, should live in different parts of the system. The Single Responsibility Principle applies that idea at the level of a single class: one class, one responsibility, one reason to change.
Why is the Single Responsibility Principle important?
Respecting the SRP produces code that is modular, cohesive, and resilient to change. Small, focused classes are easier to read, test, reuse, and refactor, and a change to one responsibility is far less likely to introduce a bug in an unrelated feature.
Conclusion
The Single Responsibility Principle is the foundation the other four SOLID principles build on. In C# and .NET it comes down to a practical habit: keep each class focused on one job, keep domain objects free of cross-cutting concerns, and let metrics like LCOM and class size tell you when a class has quietly grown into a god class.
You don’t need a perfect definition of “responsibility” to apply the SRP. Watch for classes that change for several unrelated reasons, split them along the lines of the people and business functions that drive those changes, and your codebase becomes easier to read, test, and extend. That is the whole payoff of the SRP, and the reason it stays the first principle worth mastering in SOLID design.



Very good series of post on SOLID, Patrick! Loving it!