Solid series: Single Responsibility Principle (SRP)

Pretend you’re constructing a house; you could put up some walls and a roof and say it’s all good. It may stand for a while, but what about when a storm comes? In codebase terms, this is where SOLID principles come into play.

SOLID principles are the architectural blueprint of a code. SOLID is an acronym that encapsulates these principles: single responsibility, open-closed, Liskov substitution, interface segregation, and dependency inversion.

They ensure that the code is not only strong but also robust. Think about it. Would you prefer a house with a fragile foundation that could collapse at the slightest breeze? Or a house with a strong foundation that could survive any storm?

The Single Responsibility Principle, or SRP, is the first letter of the SOLID design principles. I’d say it’s one of the most important principles you must understand to write clean and well-understandable code.

Let’s dive a little deeper.

What is SRP (Single Responsibility Principle)?

In short, the SRP says that a class should have one reason to change. This definition is easy to comprehend, but let’s explore the key concepts of SRP further.

What does “responsibility” mean in software design?

SRP clarifies “responsibility,” which is the role of a class in the system. The principle states that this responsibility must be well defined.

To comprehend what a responsibility within a system entails, below are some concrete aspects of a system:

  • Authentication
  • Data management

These two are separate responsibilities of a system, they are different so you can have a class for each of these responsibilities.

What does “one reason to change” mean?

Imagine a single employee of a company is responsible for product management, IT support, and human resource management.

If the company is planning to increase its product management capabilities, it indicates that the employee will be given more work. If IT support is lacking, the employee has to work harder. This means the job will be extremely overwhelming and daunting.

Now imagine a situation where the organization decides to have a focused team for each of the following:

  • Supporting and developing the company’s product.
  • Providing IT support for technical issues.
  • Human relations and management.

One can expect focus, efficiency, and great results because each team is specialized in specific tasks and goals.

SRP in practice

Now that we’ve discussed the theory behind single responsibility (SRP), let’s examine it in practice. We’ll start with an example of an SRP violation in which a class holds multiple responsibilities, and then refactor it to follow SRP correctly.

Example of an SRP violation

Let’s take the following UserService class in TypeScript. It balances several different roles, including:

  • User authentication
  • Database operations
  • Sending welcome emails

This is a violation of SRP because that class has more than one responsibility. These responsibilities should ideally be separated:

class UserService {
  constructor(private database: any) {}
  // Register a user
  registerUser(username: string, password: string): void {
    const hashedPassword = this.hashPassword(password);
    this.database.save({ username, password: hashedPassword });
    this.sendWelcomeEmail(username);
  }
  // Hashes password
  private hashPassword(password: string): string {
    return `hashed_${password}`; // Simulating hashing
  }
// Sends a welcome email
  private sendWelcomeEmail(username: string): void {
    console.log(`Sending welcome email to ${username}`);
  }
}

Refactoring to follow SRP

When we finally apply SRP, we should separate concerns into different classes:

  • UserRepository – This is used to perform database operations
  • AuthService – Handles authentication
  • EmailService – Sends emails:
    // Database operations
    class UserRepository {
      save(user: { username: string; password: string }) {
        console.log("User saved to database:", user);
      }
    }
    // Authentication
    class AuthService {
      hashPassword(password: string): string {
        return `hashed_${password}`; // Simulating hashing
      }
    }
    // Email sending
    class EmailService {
      sendWelcomeEmail(username: string): void {
        console.log(`Sending welcome email to ${username}`);
      }
    }
    // UserService delegating responsibilities
    class UserService {
      constructor(
        private userRepository: UserRepository,
        private authService: AuthService,
        private emailService: EmailService
      ) {}
      registerUser(username: string, password: string): void {
        const hashedPassword = this.authService.hashPassword(password);
        this.userRepository.save({ username, password: hashedPassword });
        this.emailService.sendWelcomeEmail(username);
      }
    }
    // Usage
    const userService = new UserService(
      new UserRepository(),
      new AuthService(),
      new EmailService()
    );
    userService.registerUser("JohnDoe", "securePassword");

How to detect SRP violations in actual projects

SRP seems quite simple to comprehend and use, but identifying SRP violations can be intimidating. In this section, we will provide some signs to look out for to know if a class is doing more, and thus violating the single responsibility principle.

Class changes for multiple reasons

When a class has more than one reason to change, it breaks the single responsibility design principle.

The class, in other words, is doing too much and wearing too many hats. For example, you have a class responsible for sending email alerts, user authentication, user authorization, and database transactions. You could already tell that it’s far too much responsibility for a single class, which goes against everything SRP stands for.

Too many dependencies in the class

If a class has too many dependencies, this can be an issue for maintainability. Using multiple third-party services or libraries within a class can lead to the class growing and becoming complex. Now, these services are very tightly coupled with the class, and it becomes quite difficult to change the class without affecting the other classes in the system.

Single Responsibility Principle (SRP) in various programming languages

All Object-Oriented Programming (OOP) languages follow the SOLID design principles to their core. However, implementation differs across languages. In this section, we will see a code implementation of the SRP in Python, Java, TypeScript, and C#

Python: Using well-structured classes

SRP states that a workable class has only one responsibility, and thanks to Python’s dynamic and flexible nature, we can easily implement it.

Here’s an example of Python as an SRP-compliant design approach:

class UserRepository:
    def save(self, user):
        print(f"Saving user: {user}")
class AuthService:
    def hash_password(self, password):
        return f"hashed_{password}"
class EmailService:
    def send_welcome_email(self, username):
        print(f"Sending email to {username}")
class UserService:
    def __init__(self, user_repo, auth_service, email_service):
        self.user_repo = user_repo
        self.auth_service = auth_service
        self.email_service = email_service
    def register_user(self, username, password):
        hashed_password = self.auth_service.hash_password(password)
        self.user_repo.save({"username": username, "password": hashed_password})
        self.email_service.send_welcome_email(username)
# Usage
user_service = UserService(UserRepository(), AuthService(), EmailService())
user_service.register_user("JohnDoe", "secure123")

Java: Enforcing SRP with interfaces and abstract classes

Java’s strict type system and interface-driven design help structure code to follow SRP.

Here’s an SRP-compliant approach in Java:

interface UserRepository {
    void save(User user);
}
class DatabaseUserRepository implements UserRepository {
    public void save(User user) {
        System.out.println("Saving user to database: " + user.getUsername());
    }
}
class AuthService {
    public String hashPassword(String password) {
        return "hashed_" + password;
    }
}
class EmailService {
    public void sendWelcomeEmail(String username) {
        System.out.println("Sending welcome email to " + username);
    }
}
class UserService {
    private UserRepository userRepository;
    private AuthService authService;
    private EmailService emailService;
    public UserService(UserRepository userRepository, AuthService authService, EmailService emailService) {
        this.userRepository = userRepository;
        this.authService = authService;
        this.emailService = emailService;
    }
    public void registerUser(String username, String password) {
        String hashedPassword = authService.hashPassword(password);
        userRepository.save(new User(username, hashedPassword));
        emailService.sendWelcomeEmail(username);
    }
}

TypeScript: Keeping frontend services modular

SRP helps to keep services and modules independent in TypeScript, which makes frontend code maintainable.

Here’s the SRP-compliant approach in TypeScript:

class UserRepository {
  save(user: { username: string; password: string }): void {
    console.log(`User saved: ${user.username}`);
  }
}
class AuthService {
  hashPassword(password: string): string {
    return `hashed_${password}`;
  }
}
class EmailService {
  sendWelcomeEmail(username: string): void {
    console.log(`Sending email to ${username}`);
  }
}
class UserService {
  constructor(
    private userRepository: UserRepository,
    private authService: AuthService,
    private emailService: EmailService
  ) {}
  registerUser(username: string, password: string): void {
    const hashedPassword = this.authService.hashPassword(password);
    this.userRepository.save({ username, password: hashedPassword });
    this.emailService.sendWelcomeEmail(username);
  }
}
// Usage
const userService = new UserService(
  new UserRepository(),
  new AuthService(),
  new EmailService()
);
userService.registerUser("JohnDoe", "securePass");

C#: Using SRP in enterprise applications

C# encourages clean architecture with interfaces and dependency injection, enforcing SRP naturally.

Here’s the SRP-compliant approach in C#:

public interface IUserRepository {
    void Save(User user);
}
public class UserRepository : IUserRepository {
    public void Save(User user) {
        Console.WriteLine($"User saved: {user.Username}");
    }
}
public class AuthService {
    public string HashPassword(string password) {
        return "hashed_" + password;
    }
}
public class EmailService {
    public void SendWelcomeEmail(string username) {
        Console.WriteLine($"Sending welcome email to {username}");
    }
}
public class UserService {
    private readonly IUserRepository _userRepository;
    private readonly AuthService _authService;
    private readonly EmailService _emailService;
    public UserService(IUserRepository userRepository, AuthService authService, EmailService emailService) {
        _userRepository = userRepository;
        _authService = authService;
        _emailService = emailService;
    }
    public void RegisterUser(string username, string password) {
        string hashedPassword = _authService.HashPassword(password);
        _userRepository.Save(new User(username, hashedPassword));
        _emailService.SendWelcomeEmail(username);
    }
}
// Usage
UserService userService = new UserService(new UserRepository(), new AuthService(), new EmailService());
userService.RegisterUser("JohnDoe", "securePass");

Benefits of the Single Responsibility Principle

Like every software design pattern, the single responsibility principle and other SOLID principles ensure that developers start writing high-quality code.

This principle is important because it allows you to:

Reduce complexity

Breaking code into smaller, more focused units makes it easier to understand and manage your code.

Enhance reusability

When classes are focused on a single responsibility, they can be reused across your application and different projects, making utility classes highly possible.

Facilitate testing

Smaller classes with a single responsibility are much easier to test because there are fewer cases to consider.

Improve maintainability

When bugs appear (and they will), you can debug the issue much quicker when your code structure adheres to SRP. SRP ensures that developers can pinpoint the source of a bug faster, as it provides a well-organized codebase that is easy to maintain.

Common misconceptions and pitfalls of SRP

The single responsibility principle provides a solid blueprint for writing high-quality code, but many developers misinterpret or misuse it. In this section, we will review some of the common misconceptions of SRP that developers should be mindful of.

Misunderstanding SRP as “one method per class”

This is a common misconception among developers, especially those who are new to the SRP. SRP talks about a class having only one responsibility, which means that everything about a single class should be about only one responsibility. A class can have as many methods as needed, as long as they all work together to ensure that the only responsibility is done well and effectively.

Over-abstracting

Many developers create needless classes in the name of using the single responsibility principle (SRP). Over-abstraction is the wrong way of applying SRP. Too much abstraction makes it difficult to understand the code.

Wrong abstraction level

SRP at the wrong abstraction level occurs when a developer applies the principle of separation at a level that doesn’t align with the structure of the system. The code may technically follow SRP by having only one “reason to change,” but that reason may be so trivial, or detached from the business logic that it adds unnecessary complexity rather than clarity. The wrong abstraction level can pose some serious problems, as maintenance becomes harder and components may lack flexibility.

SRP in large-scale systems

The SRP provides an awesome experience in small projects. However, the real usefulness comes when the project’s complexity starts to grow.

SRP applies to system architecture as well as individual classes in large programs, ensuring that various components address various issues.

Below, we will examine how SRP is effectively used in modular monolithic architecture, which is a common pattern in small to medium projects, and microservices architecture, which is most common in industry-level applications.

SRP in microservices architecture

At the service level, each service is in charge of a specific business capability. Microservices inherently enforce SRP.

Here’s how SRP works in microservices:

  • Each microservice handles one specific function (e.g., user authentication, payments, order administration)
  • Modification to one service does not affect another service in any way because one service is loosely coupled to another
  • Each service may be independently deployed and upgraded, consequently making scalability much easier

Let’s consider an example. A PaymentService executes transactions, whereas a UserService solely manages operations pertaining to users. If the payment logic needs to be changed, the UserService will not be impacted.

SRP in modular monoliths

The application is still a single unit in modular monoliths, but it is separated into SRP-compliant, well-structured modules.

Here’s how SRP works in modular monoliths:

  • Each module has a clear boundary and handles a specific responsibility (e.g., UserModule, BillingModule)
  • Modules communicate via well-defined interfaces, reducing unnecessary dependencies
  • The code remains scalable and maintainable, even within a monolithic structure

Let’s think of an example. An e-commerce platform can contain distinct modules for users, products, and orders, each focusing on a particular responsibility rather than a single monolithic service managing everything.

Best practices for applying SRP

Effective implementation of the Single Responsibility Principle (SRP) requires careful planning and sensible decision-making, in addition to class separation.

Identify responsibilities clearly before splitting classes

Make sure the responsibilities are separate before dividing a class into many parts. Related functions may belong together, so not all multi-method classes violate SRP. Divide a class into distinct classes if it covers several business issues, but refrain from needless fragmentation.

Use layers and modules to separate concerns

Enforcing SRP at the architectural level helps avoid bloated classes in large applications. A layered architecture, which keeps the user interface, business logic, and data access distinct, is best practice. Additionally, modularizing your code organizes related functionality into well-structured modules or services.

Apply SRP alongside other SOLID principles

SRP works best in combination with other SOLID principles, such as:

  • Open-Closed Principle (OCP) — Classes should be open for extension but closed for modification
  • Dependency Inversion Principle (DIP)Depends on abstractions, not concrete implementations.

It is best to use dependency injection to pass services into classes instead of hard-coding dependencies.

Refactor when necessary

Overapplying SRP too early can lead to excessive abstraction, making the system harder to manage. It is best practice to start simple, then refactor when you notice clear SRP violations. Use code smells (e.g., a class with too many dependencies) as signals for refactoring.

Write unit tests to validate SRP compliance

If a class has too many responsibilities, it may be checked with a good test suite. SRP may be broken if a single class needs to be tested for several independent functionalities. If a test becomes complicated because it covers several issues, you should probably rewrite the class.

Conclusion

A key idea in software design is the Single Responsibility Principle (SRP), which guarantees that any class, module, or component has just one reason to change. By using SRP, developers can produce code that is easier to debug, test, and extend, making it clearer, more maintainable, and scalable.

To provide flexibility and long-term maintainability, SRP carefully separates concerns rather than merely dividing classes.

The post Solid series: Single Responsibility Principle (SRP) appeared first on LogRocket Blog.

 

This post first appeared on Read More