Mastering the Single Responsibility Principle: Code with Clarity

Mastering the Single Responsibility Principle: Code with Clarity

If you're short on time, feel free to scroll to the bottom and check out the final thoughts—you might gain some valuable insights in just a minute.

For those who prefer a deeper dive, sit back, relax, and enjoy the read!


This is Part 1 of a five-part series on the SOLID principles, where we’ll capture the essence of each one. Today, our focus is on the Single Responsibility Principle (SRP).

Change is the only constant.

It can't be emphasized enough: change in code is inevitable. We've all seen requirements evolve—whether it's adding new features, enhancing existing ones, or fixing bugs. Software continues to grow and adapt.

The more responsibilities a class has, the more reasons there are for change, and the higher the chances of unintended side effects. Eventually, with all these modifications, a class can become so convoluted that even a minor tweak feels like it needs to be done with surgical precision to avoid disaster.

That's where the Single Responsibility Principle comes in to guide you.

What is the Single Responsibility Principle?

After teasing why our code should be carefully structured, let’s dive into the official definition, as proposed by Robert C. Martin, aka Uncle Bob.

The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change.

In essence, this means we should write our code so that a class changes for one reason only.

Sounds simple, right?

Well, don’t be fooled by the simplicity of the name. This seemingly straightforward principle is often the most misunderstood one.

The complexity arises from confusion around the word "reason." In fact, Uncle Bob himself had to clarify the concept in a blog post to clear up the confusion (Ref: Uncle Bob’s blog).

So, when we talk about "reasons to change," what do we mean?

  1. Does fixing a bug qualify as a reason to change?
  2. Does refactoring fall under this category?

Uncle Bob points out that:

Code is not responsible for bug fixes or refactoring. Those things are the responsibility of the programmer, not of the program.

That’s true. Code is responsible for the functionality it provides. Or, to put it differently:

Who must the design of the program respond to?

Consider the example provided in the article, which imagines a typical business organization with a CEO at the top and CFO, COO, and CTO reporting to them.

A simple Employee class might offer the following methods:

public class Employee {
  public Money calculatePay();
  public void save();
  public String reportHours();
}
        

  • calculatePay: Determines total payment for the employee, including base salary, overtime, and bonuses ?? — relevant to the CFO.
  • save: Stores employee information in a database ?? — relevant to the CTO.
  • reportHours: Captures the employee’s working hours ?? — relevant to the COO.

The essence of the Single Responsibility Principle is that it’s about people.

We want software to be structured so that when a change is requested for a class or module, it originates from a single person (or group) representing a distinct business function.

Why is this important?

Imagine a piece of code being reused by different functions, each responding to two or more stakeholders. One group requests a modification, but this change unknowingly disrupts another group’s workflow. The result? A ripple effect that could get someone fired—or even worse, you! ??

Minor side effects in shared code can have a detrimental impact on business operations. Someone might wonder why an unrelated part of their workflow started malfunctioning after a seemingly small change.

If this scenario sounds familiar, your code is probably not adhering to the Single Responsibility Principle.

To put it another way:

Gather together the things that change for the same reasons. Separate those that change for different reasons.

The goal is to increase cohesion among things that change for the same reasons and decrease coupling between different business changes.

Bottom Line:

  • Reasons for change are people.
  • People request changes.
  • People care about changes.
  • People are impacted by changes.

We want to ensure that when one group requests a change, it doesn’t inadvertently affect another group.

How Do People Ignore SRP?

  • We get a task.
  • We figure out which class the change should go into.
  • We identify the method that needs to be modified.
  • And then, we make the change.

Any issues so far?

At first glance, everything seems fine. That’s how changes are usually made, right?

Not quite!

There are a few critical things we often forget to consider.

Here are some questions you should always ask:

  • Does this change really belong to an existing class, or does it require a completely new class/module? Just because a class seems like it can handle the change, we often take shortcuts and modify the existing class to avoid the effort of creating and managing a new one—even when that’s the better choice.
  • Even if it belongs to an existing class, does it really fit into an existing method, or are we just being lazy by not creating a new one?

These small, seemingly harmless shortcuts can snowball into bigger problems, leading to tightly coupled code that becomes difficult to maintain or extend. By skipping the proper evaluation of responsibilities, we unintentionally violate the Single Responsibility Principle—and that's when the real issues begin.

How one can go wrong with SRP?

As I mentioned at the start of this series, treat these principles as guidelines, not strict rules. Blindly following the Single Responsibility Principle can lead to over-engineering, oversimplification, and unnecessary creation of classes that could easily be combined—ironically breaking SRP in the process.

One of the most common misapplications of SRP is creating a separate class or module for every little task.

Some even go as far as creating classes with just one method. But SRP never suggests that a class should have only one method.

The number of methods isn't the deciding factor. A class with many cohesive methods that serve a single responsibility still adheres to SRP. On the other hand, a class with just two methods, if they serve different purposes, violates SRP.

Let’s look at an example to see this in action.

Example to understand SRP

Let's sharpen our understanding of the Single Responsibility Principle by walking through a few examples. Each snippet may seem fine at first, but we'll uncover the SRP violations lurking underneath.

Feel free to think through the problems before reading the solution—you might appreciate SRP even more!

Example 1

public class EmployeeService {
    
    void addEmployee(String name) {
        /*
            Code to 
            - create DB connection 
            - save employee into database 
         */
    }
}
        

At first glance, this code seems reasonable. But what happens if, down the road, we decide to migrate from one database to another? Does EmployeeService really need to know or care about database connections?

Of course not.

The service's sole responsibility is to add an employee, not manage how or where the data is stored. It doesn’t care about the underlying database. This logic should be outsourced to something like EmployeeDao, which handles all data access operations.

Fixed Code

public class EmployeeService {

    void addEmployee(String name) {
        /*
            employeeDao.save(employee)
         */
    }
}
        

Example 2


public class EmployeeService {

    void addEmployee(String name) {
        Employee employee = new Employee(name);
        saveEmployee(employee);
        payJoiningBonus();
    }

    void saveEmployee(Employee employee) {
        /*
            employeeDao.save(employee)
         */
    }

    void payJoiningBonus() {
        /*
            code to connect to payment gateway and transfer money
         */
    }
}
        

Here, we’ve successfully separated concerns by delegating database operations to the Dao class. However, the issue is now with the payJoiningBonus method.

Much like in the previous example, does EmployeeService really need to know how bonuses are paid or which payment gateway is being used? What happens if you switch payment providers in the future?

Again, EmployeeService shouldn’t manage payments. We need a PaymentService to handle this.

Fixed code

Additionally, it’s worth mentioning that PaymentService should be designed to handle various types of payments, not just for employees—think vendors, third parties, etc.

void payJoiningBonus() {
        /*
            paymentService.pay(employee);
         */
    }
        

Example 3

Another common SRP debate: Do we really need a new class?

The answer often depends on the complexity of the use case.

void addEmployee(String name) {
        Employee employee = new Employee(name);
        validateEmployee(employee);
        saveEmployee(employee);
        paySalary();
    }
    
    private void validateEmployee(Employee employee) {
        /*
        all validations
         */
    }
        

Does validation really belong in the EmployeeService?

Some would argue that it does, while others would advocate for creating a dedicated EmployeeValidator class to handle all validation logic.

The answer depends on the scale. If the validations are minimal, keeping them in EmployeeService may be acceptable. However, as the number of validations grows, it makes more sense to separate concerns into a dedicated EmployeeValidator class. This keeps things modular and cohesive, ensuring that any new validations can be easily added without bloating the EmployeeService.

It also helps future developers quickly find and manage all validations in one place.

Why Should You Care About SRP?

Now that we’ve covered what the Single Responsibility Principle is and how to avoid common pitfalls, let’s look at why it matters in practice:

  • Easier to Understand Classes with well-defined, focused responsibilities are more intuitive and easier to reason about. The name of the class alone should give you a good idea of its purpose, making it easier to navigate the codebase.
  • Simpler Implementation When you clearly define the responsibilities of a class without exceptions or special cases, implementation becomes straightforward. You’ll spend less time juggling complexity and more time writing meaningful code.
  • Fewer Bugs and Side Effects By separating concerns, the likelihood of introducing bugs or unintended side effects when modifying the code decreases significantly. Focused classes are more predictable and less fragile.
  • Faster Development Clean, modular code speeds up development. With fewer cross-concerns to worry about, changes can be made more efficiently, allowing you to ship features faster.

Wrapping It Up

That was quite a bit to take in, wasn’t it? Let’s quickly wrap it all up.

The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change.

But remember, when we say “one reason to change,” we’re not talking about bug fixes, refactoring, or feature additions. The “reason” here refers to the core functionality or responsibility of a class or module. It's the single, primary thing that your module should focus on, and that should be the only reason it changes.

We can also think about SRP in terms of the people behind the change:

Your software should be designed in a way that when a change is requested, it originates from a single person or group representing a distinct business function. The class should serve their specific needs and not spill over into other areas.

From an implementation perspective:

Gather together things that change for the same reasons. Separate those that change for different reasons.

Different developers will interpret and apply SRP in their own way, and that’s okay. The key is to remember that SRP exists to help you structure your code in a way that is understandable, minimizes disruption when changes happen, and keeps bugs or side-effects in check.

And there you have it—the Single Responsibility Principle in a nutshell.


Special Thanks ????

If you’ve made it this far, give yourself a round of applause! ?? While I had a great time writing this, I fully understand that not everyone has the patience to dig deep into these concepts. But you stuck around, and that’s worth celebrating.

The takeaway here: SRP isn’t just about splitting everything into smaller pieces. It’s about making your code resilient, maintainable, and future-proof by keeping responsibilities clear and distinct.


If you enjoy this newsletter, leave a like below ??, share your thoughts in the comments section ??, and don’t forget to follow for more such content! ????

Keep showering your love on this one, and I’ll see you next time with something new. Till then, have fun and keep the Joy of Engineering alive within you! ???

#SoftwareEngineering #Learning #Design #Java #Innovation #JoyOfEngineering

要查看或添加评论,请登录

Umang Ahuja的更多文章

社区洞察

其他会员也浏览了