What is Single Responsibility Principle
SRP, or Single Responsibility Principle, states that a class should have only one reason to change. In other words, the class should represent one cohesive responsibility. If a class changes for multiple unrelated reasons, it is likely handling more than one responsibility.
When unrelated responsibilities are mixed in one class, the class becomes less cohesive. A change for one concern can affect code that serves another concern, which makes the class harder to test, maintain, and modify safely.
Mental Model
The mental model of SRP is to think about
how many reasons a class has to change.
Another way to think about it is to ask:
- How many kinds of changes could be requested for this class?
- How many sources of change affect this class?
In Uncle Bob’s explanation here Uncle Bob SOLID principles, these sources of change are often described in terms of different actors. An actor represents a person or group that interacts with the system and may request changes, such as business stakeholders, UI designers, database engineers, or other teams. This is a useful lens, because different actors often introduce different kinds of change. However, the deeper idea is still the same: a class should not be pulled by multiple unrelated reasons to change.
If a class needs to respond to requests from multiple unrelated actors, that can be a sign that the class is handling multiple responsibilities. Each actor may request changes for different reasons, which can cause the class to change in unrelated ways.
Because of that, a class should ideally serve one cohesive source of change. In many cases, that also means it mainly serves one actor, or one closely related group of actors.
A class is cohesive when its methods naturally belong together and tend to change together for the same reason.
Difference between SoC (Separation of Concern)
Since some of you may have come to this article from my series What I’m Learning About Writing Better Structured Code: A Learning Series may have already read about Seeing the problem: An Introduction to Separation of Concerns , you might be wondering what the difference is between SRP and SoC. This section is only a quick explanation of the difference. I will write another article about it later.
The difference is that SoC is a broader idea, while SRP is a more specific design principle.
Separation of Concerns is about separating different kinds of work in a system, so each part focuses on its own concern. For example, UI rendering, business logic, networking, and data storage are different concerns, and we usually try to keep them separated.
The Single Responsibility Principle is more specific. It helps us judge whether a particular unit, such as a class, forms one cohesive responsibility and has one reason to change, while SoC is a broader way of thinking about separating different kinds of work across a system.
So they are related, but they are not exactly the same. SoC helps us think about separating different concerns in general, while SRP helps us judge whether a specific class or unit is trying to handle too much.
OOP missconception.
lets look at this class as an example.
class PayslipProcessor {
func calculateNetSalary(for employee: Employee) { ... }
func generatePayslipContent(for employee: Employee) { ... }
func sendPayslip(to employee: Employee) { ... }
}
This class may look fine right? Its a PayslipProcessor object, and all of its functions seem related to processing payslips.
I used to think like that too. Maybe one of the reasons is because that is how OOP (Object Oriented Programming) is often introduced in college and in many tutorials. A class is usually explained as a representation of a real-world object. Because of that, it's easy to think, “If this is a payslip processor object, then it makes sense if it contains all functions related to payslip processing”.
And if we continue with that way of thinking, it can still sound reasonable. In real life, someone in payroll might calculate salary, prepare the payslip content, and send it to the employee. So from that pov, putting all of those functions into one class can feel correct. But this is where the problem starts. The mistake is that we group things based on the fact that they look related on the surface. Yes, they are all related to payslip processing. But that does not automatically mean they are the same responsibility.
These are separate responsibilities not simply because they are different actions, but because they are affected by different kinds of changes. Salary calculation changes when payroll rules change. Payslip content changes when content or compliance requirements change. Payslip delivery changes when the delivery mechanism or integration changes.
This is why thinking about a class only as a real-world object is sometimes not enough. It can push us to put too many responsibilities into one class just because they look like they belong together. That is why SRP gives us a better way to judge it.
Instead of only asking,
“Does this function belong to this object?”
we should also ask
“How many reasons does this class have to change?”
Implementing SRP
So how do we actually implement SRP?
The main idea is not to split a class just because it has many methods, or because the file looks long. That can happen, but that is not the real point.
The real point is to look at a class and ask:
- How many kinds of changes could be requested for this class?
- How many sources of change affect this class?
Let’s go back to the previous example:
class PayslipProcessor {
func calculateNetSalary(for employee: Employee) { ... }
func generatePayslipContent(for employee: Employee) { ... }
func sendPayslip(to employee: Employee) { ... }
}
At first, this class looks good because everything is still about payslips. But if we inspect it using the SRP mental model, we can see that the class is being pulled by different kinds of changes.
The salary calculation can change when payroll rules change. Maybe finance changes the overtime rule, tax rule, or deduction formula.
The payslip content can change when HR or compliance requests a different format, adds a required field, or changes the wording.
The delivery requirements can change. For example, the payslip may need to be emailed, uploaded to another system, or sent through a different channel.
These changes do not come from the same reason. They may all happen in the same feature area, but they are still different responsibilities. That is the important part. SRP is not telling us to split code randomly. It is telling us to separate parts that change for different reasons.
So instead of keeping all of those things inside one class, we can separate them by responsibility:
class SalaryCalculator {
func calculateNetSalary(for employee: Employee) { ... }
}
class PayslipContentGenerator {
func generatePayslipContent(for employee: Employee) { ... }
}
class PayslipSender {
func sendPayslip(to employee: Employee) { ... }
}
This is one possible way to create clearer responsibility boundaries. In a real system, the exact split may vary depending on how the domain and workflow are designed, but the main idea stays the same:
separate parts that change for different reasons.
The exact responsibility boundary is often contextual. What matters is not following a fixed pattern, but identifying which parts of the code change together and which change independently.
Now each class has a more focused reason to change.
- If payroll rules change, we know the first class is the one that should be affected.
- If the payslip content changes, we know the second class is the one that should be affected.
- If the delivery mechanism changes, we know the third class is the one that should be affected.
That is a much better boundary. And that is the core of implementing SRP:
we separate code based on different sources of change, not just based on whether functions look related at a glance.
This is also why SRP is not just about making classes small. A class can still have multiple methods and still follow SRP, as long as those methods support the same responsibility and are affected by the same kind of change. Different methods or steps do not automatically mean different responsibilities. If they always change together for the same reason, they may still belong in the same class.
The Orchestration
After separating responsibilities into focused classes, we still need a way to coordinate them into one complete workflow. This is where orchestration comes in.
Orchestration means one part of the code is responsible for managing the flow between these smaller classes. It does not take over their responsibilities. Instead, it calls them in the right order to complete one use case.
A simplified example might look like this:
class PayslipService {
let salaryCalculator: SalaryCalculator
let contentGenerator: PayslipContentGenerator
let sender: PayslipSender
init(
salaryCalculator: SalaryCalculator,
contentGenerator: PayslipContentGenerator,
sender: PayslipSender
) {
self.salaryCalculator = salaryCalculator
self.contentGenerator = contentGenerator
self.sender = sender
}
func processPayslip(for employee: Employee) {
let salary = salaryCalculator.calculateNetSalary(for: employee)
let content = contentGenerator.generatePayslipContent(for: employee)
sender.sendPayslip(to: employee)
}
}
The important point is that PayslipService coordinates the workflow, but the responsibility of calculation, content generation, and delivery still stays in their own classes.
This is only a simplified example to show the role of orchestration. In a real system, the design can be improved further depending on the architecture and the complexity of the workflow. I will cover that in another article in this SOLID series.
From the Writer
Hello, allow me to introduce myself. I’m Cakoko. We’ve reached the end of this article, and I sincerely thank you for taking the time to read it.
If you have any questions or feedback, feel free to reach out to me directly via email at cakoko.dev@gmail.com. I’m more than happy to hear your thoughts, whether they are about my English writing, the technical ideas in this article, or anything I may have misunderstood. Your feedback will help me grow.
I look forward to connecting with you in future articles. Btw, I’m a mobile developer, final year Computer Science student, and an Apple Developer Academy @ IL graduate. I’m also open to various opportunities such as collaborations, internships, or full-time positions. It would make me very happy to explore those possibilities.
Until next time, stay curious and keep learning.
Open for Feedback
This article is part of my personal learning journey. It may not be completely accurate or perfect, and that is okay. I’m sharing what I’ve learned so far in the hope that it can also help others who are exploring similar topics.
If you have any feedback, suggestions, or corrections, I would truly appreciate them. I’m always open to learning more and improving along the way.
Top comments (0)