Skip to content

SRP - The Single Responsibility Principle

A class should have one reason to change, which really means one group of people who would ask you to change it.

Builds on Coupling and Cohesion

Stage 2core2 min read

The code

package SOLID.SRP.GoodCode;

// Invoice.java: owns invoice calculation and presentation rules.
public class Invoice {
    private double amount;

    public Invoice(double amount) {
        this.amount = amount;
    }

    public void generateInvoice() {
        System.out.println("Invoice generated & printed for amount " + amount);
    }
}

// InvoiceRepository.java: owns database storage details.
public class InvoiceRepository {
    public void saveToDatabase() {
        System.out.println("Saving invoice to database");
    }
}

// EmailService.java: owns email delivery details.
public class EmailService {
    public void sendEmailNotification() {
        System.out.println("Sending email notification for invoice");
    }
}

Main now uses one class for each job:

Invoice invoice = new Invoice(100.0);
invoice.generateInvoice();

InvoiceRepository repository = new InvoiceRepository();
repository.saveToDatabase();

EmailService emailService = new EmailService();
emailService.sendEmailNotification();

These are separate objects because storage and email are separate jobs. The calls happen in the same order as before. What changed is where each kind of knowledge lives.

What got better

Each class now answers to only one actor (one group of people who ask for its changes):

ClassChanges whenActor
Invoicetax or billing rules changeFinance
InvoiceRepositorystorage or schema changesPlatform / DBA
EmailServiceemail provider or template changesCRM / Marketing
  • Changes stay in one place. Switching from SMTP to SES (two ways to send email) touches only EmailService.
  • Tests stay small. new Invoice(499.0) needs no database and no mail server.
  • Bugs are easier to find. A storage failure points to the repository, not to one class that does everything.
  • Classes can be reused on their own. You can use invoice calculation without dragging in email code.

We split by reasons to change, not by a rule like "one method per class." Invoice could have several methods, such as subtotal(), tax() and total(), and still follow SRP if finance owns all those rules.

What you would add in real code

This example keeps the method signatures tiny so the split is easy to see. Real code would usually do more:

  • Pass the invoice to the other classes. The repo's saveToDatabase() and sendEmailNotification() take no arguments, so they never see the invoice. A real version might look like repository.save(invoice) and emailService.send(invoice, "ravi@example.com"). Those two method names are only examples, not in the repo.
  • Add a coordinator such as BillingService.bill(invoice, email) to run the steps in order. It does not calculate, save or send anything itself. Its one job is the billing workflow.
  • Use a type that is safe for money, such as BigDecimal, or store paise as a long, instead of double.
  • Put email or saving behind an interface when there is a real second implementation, or when tests need a fake. That idea is DIP.

There is a trade-off. To follow one flow you now open several files. That is worth it when different actors really change the parts on their own. If one small team always changes and tests all three together, splitting right away may only add clutter.

Compare with BadCode, and read SRP-Notes.md for more examples.

In this topic

After code4 files

GoodCode/EmailService.java9 linesGitHub
package SOLID.SRP.GoodCode;public class EmailService {    public void sendEmailNotification() {        System.out.println("Sending email notification for invoice");    }}
GoodCode/Invoice.java16 linesGitHub
package SOLID.SRP.GoodCode;public class Invoice {    private double amount;    public Invoice(double amount) {        this.amount = amount;    }    // Additional Functionality    public void generateInvoice() {        System.out.println("Invoice generated & printed for amount " + amount);    }}
GoodCode/InvoiceRepository.java9 linesGitHub
package SOLID.SRP.GoodCode;public class InvoiceRepository {    public void saveToDatabase() {        System.out.println("Saving invoice to database");    }}
GoodCode/Main.java20 linesGitHub
package SOLID.SRP.GoodCode;/** * Good: Invoice only generates; repository saves; email service sends. Single responsibility each. */public class Main {    @SuppressWarnings("unused")    public static void main(String[] args) {        Invoice invoice = new Invoice(100.0);        invoice.generateInvoice();        InvoiceRepository repository = new InvoiceRepository();        repository.saveToDatabase();        EmailService emailService = new EmailService();        emailService.sendEmailNotification();    }}

Done with this topic?

How well do you know it?

Practise 6 questions