LLDSOLID PrinciplesSingle Responsibility Principle (SRP)

Single Responsibility Principle (SRP)

SOLID Principles

Have you ever encountered a class that seems to do everything? It fetches data from the database, performs complex business calculations, formats that data into HTML or JSON, and maybe even logs errors to a file. This all-in-one class, often called a "God Class," is a common anti-pattern.

Consider this Report class:

public class Report {
    private String content;

    public void generateReportData() {
        // Business logic to calculate and gather data
        this.content = "Calculated report data...";
        System.out.println("Data generation complete.");
    }

    public void saveToFile(String filePath) {
        // Persistence logic to write the report to a file
        System.out.println("Saving report to " + filePath);
        // ... file I/O operations
    }

    public void sendByEmail(String recipient) {
        // Networking and formatting logic to send an email
        System.out.println("Emailing report to " + recipient);
        // ... SMTP connection, email formatting, etc.
    }
}

This class seems convenient at first. But what happens when a requirement changes?

  • Multiple developers might need to modify this class for different reasons, leading to merge conflicts.
  • A bug in the file-saving code could bring down the emailing functionality.
  • The class is nearly impossible to unit test. How do you test data generation without actually trying to save a file or send an email?

This class is brittle, hard to test, and difficult to understand because it has too many responsibilities.

The Principle: One Reason to Change

The Single Responsibility Principle, as defined by Robert C. Martin, states:

A class should have only one reason to change.

"Doing only one thing" is too vague. A better way to frame it is to think about actors or stakeholders.

  • The Chief Financial Officer (CFO) is an actor who cares about the rules for data generation.
  • A Database Administrator (DBA) is an actor who cares about the persistence logic.
  • A UI/UX designer might be an actor who cares about the presentation or format of the report.

Since these three actors would request changes for different reasons, their concerns should live in different classes. A class should be responsible to a single actor.

Separating Concerns

Let's refactor our Report class by separating its responsibilities into distinct classes, each with a clear purpose.

Step 1: The Data-focused Class This class is only responsible for the core data and the business logic to generate it.

public class Report {
    private String content;

    public void generateReportData() {
        // Business logic to calculate and gather data
        this.content = "Calculated report data...";
        System.out.println("Data generation complete.");
    }

    public String getContent() {
        return content;
    }
}

Step 2: The Persistence-focused Class This class's only job is to handle how a report is stored.

public class ReportPersistence {
    public void saveToFile(Report report, String filePath) {
        // Persistence logic to write the report to a file
        System.out.println("Saving report to " + filePath);
        // ... file I/O operations using report.getContent()
    }
}

Step 3: The Delivery-focused Class This class is only concerned with sending the report.

public class ReportEmailer {
    public void sendByEmail(Report report, String recipient) {
        // Networking and formatting logic to send an email
        System.out.println("Emailing report to " + recipient);
        // ... SMTP connection, email formatting using report.getContent(), etc.
    }
}

Now, a change to the email logic only affects ReportEmailer. The core Report class remains untouched and stable. Each class is easy to understand, test, and maintain.

Interview Focus: The Nuance of "Responsibility"

A key discussion point in an interview is the granularity of SRP. If taken to an extreme, you could end up with a "class explosion," where every single method gets its own class. This is not the goal.

  • Weak Interpretation: "Every method is a responsibility." This leads to overly fragmented code.
  • Strong Interpretation: "A responsibility is a cohesive set of functions that serves a single high-level purpose or actor." Our ReportPersistence class might have methods for saveToFile, loadFromFile, and deleteFile. These are multiple methods, but they all serve the single responsibility of persistence.

The skill is in defining the responsibility at the correct level. A good rule of thumb is to ask, "If I change this, what is the blast radius? Am I changing code that serves a completely different business need?" If the answer is yes, you are likely violating SRP.

How SRP Connects to System Design

  • Cohesion and Coupling: SRP is a direct strategy for achieving high cohesion (the elements inside a class are closely related) and low coupling (classes are independent of each other). These are two of the most fundamental goals of good software design.
  • Testability: Single-responsibility classes are vastly easier to test. You can test Report's calculation logic with a simple unit test. You can test ReportPersistence by mocking the Report object.
  • Architectural Layers: SRP is the same principle that drives n-tier architecture. You have a Presentation Layer, a Business/Domain Layer, and a Data Access Layer. Our refactored code is a micro-example of this architectural pattern.
  • Microservices: The philosophy behind a microservice architecture is SRP on a macro scale. Each service owns a distinct business capability (e.g., OrderService, PaymentService, NotificationService) and has only one reason to change: a change in that specific business capability.