Skip to content

Single Responsibility Principle (SRP)

SOLID Series — This article is Part 1 of 5. See also: OCP · LSP · ISP · DIP

Introduction

The Single Responsibility Principle states that a class should have only one reason to change. Coined by Robert C. Martin, it is the first and most foundational of the five SOLID principles. When a class takes on multiple responsibilities, changes to one concern inevitably ripple into unrelated areas, increasing fragility and coupling throughout the codebase.

Core Concept

A "responsibility" in SRP terms is a reason to change. If you can describe a class and find yourself using the word "and" — "this class handles user data and sends emails and generates PDFs and persists to the database" — that class has too many responsibilities. Each distinct concern should live in its own class with a well-defined boundary.

A useful heuristic: write a one-sentence description of your class. If that sentence contains more than one verb phrase tied to different actors or stakeholders, the class likely violates SRP.

Anti-Pattern — The God Class

The classic SRP violation is the "God Class" — a single class that knows too much and does too much. Consider a User class that handles its own data, persistence, email notifications, and report generation.

java
// ANTI-PATTERN: User class with too many responsibilities
public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    // Responsibility 1: User data
    public String getUsername() { return username; }
    public String getEmail() { return email; }

    // Responsibility 2: Database persistence
    public void save() {
        String sql = "INSERT INTO users (username, email, password) VALUES (?, ?, ?)";
        // Direct JDBC calls buried inside the domain object
        try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db", "root", "pass");
             PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, username);
            ps.setString(2, email);
            ps.setString(3, password);
            ps.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static User findById(Long id) {
        // More raw JDBC inside the domain model
        String sql = "SELECT * FROM users WHERE id = ?";
        // ... omitted for brevity
        return null;
    }

    // Responsibility 3: Email notifications
    public void sendWelcomeEmail() {
        Properties props = new Properties();
        props.put("mail.smtp.host", "smtp.example.com");
        Session session = Session.getInstance(props);
        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress("no-reply@example.com"));
            message.setRecipient(Message.RecipientType.TO, new InternetAddress(email));
            message.setSubject("Welcome!");
            message.setText("Hello " + username);
            Transport.send(message);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    // Responsibility 4: PDF report generation
    public byte[] generateUserReport() {
        // Directly coupled to a PDF library
        Document document = new Document();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            PdfWriter.getInstance(document, baos);
            document.open();
            document.add(new Paragraph("User Report: " + username));
            document.add(new Paragraph("Email: " + email));
            document.close();
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return baos.toByteArray();
    }
}

This class has four distinct reasons to change:

  1. The user's data model changes (new fields, validation rules).
  2. The database schema or ORM technology changes.
  3. The email provider or template changes.
  4. The PDF library or report format changes.

Any of these changes forces you to open, modify, and re-test a class that mixes completely unrelated concerns.

Correct Implementation

The fix is straightforward: split each responsibility into its own class. The User class becomes a pure data object (POJO); separate classes handle each concern.

java
// CORRECT: User is a pure domain object (POJO)
public class User {
    private Long id;
    private String username;
    private String email;
    private String password;

    public User(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    // Only getters and setters — no infrastructure concerns
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getUsername() { return username; }
    public String getEmail() { return email; }
    public String getPassword() { return password; }
}
java
// CORRECT: Persistence is isolated behind a repository interface
public interface UserRepository {
    User save(User user);
    Optional<User> findById(Long id);
    List<User> findAll();
    void delete(Long id);
}

// Concrete implementation — only reason to change: persistence strategy
@Repository
public class JpaUserRepository implements UserRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public User save(User user) {
        entityManager.persist(user);
        return user;
    }

    @Override
    public Optional<User> findById(Long id) {
        User user = entityManager.find(User.class, id);
        return Optional.ofNullable(user);
    }

    @Override
    public List<User> findAll() {
        return entityManager.createQuery("SELECT u FROM User u", User.class).getResultList();
    }

    @Override
    @Transactional
    public void delete(Long id) {
        User user = entityManager.find(User.class, id);
        if (user != null) entityManager.remove(user);
    }
}
java
// CORRECT: Email logic lives in its own service
@Service
public class EmailService {

    private final JavaMailSender mailSender;

    public EmailService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    // Only reason to change: email templates or provider configuration
    public void sendWelcomeEmail(User user) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(user.getEmail());
        message.setSubject("Welcome to Our Platform");
        message.setText("Hello " + user.getUsername() + ", welcome aboard!");
        mailSender.send(message);
    }

    public void sendPasswordResetEmail(User user, String resetToken) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(user.getEmail());
        message.setSubject("Password Reset Request");
        message.setText("Use this token to reset your password: " + resetToken);
        mailSender.send(message);
    }
}

// CORRECT: Report generation is its own concern
@Service
public class UserReportGenerator {

    // Only reason to change: report format or PDF library
    public byte[] generateUserReport(User user) {
        Document document = new Document();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            PdfWriter.getInstance(document, baos);
            document.open();
            document.add(new Paragraph("User Report"));
            document.add(new Paragraph("Username: " + user.getUsername()));
            document.add(new Paragraph("Email:    " + user.getEmail()));
            document.close();
        } catch (DocumentException e) {
            throw new ReportGenerationException("Failed to generate report for user: " + user.getId(), e);
        }
        return baos.toByteArray();
    }
}
java
// CORRECT: Spring Boot service that orchestrates the separated concerns
@Service
public class UserRegistrationService {

    private final UserRepository userRepository;
    private final EmailService emailService;
    private final UserReportGenerator reportGenerator;

    // All dependencies injected — no direct instantiation of infrastructure
    public UserRegistrationService(
            UserRepository userRepository,
            EmailService emailService,
            UserReportGenerator reportGenerator) {
        this.userRepository = userRepository;
        this.emailService = emailService;
        this.reportGenerator = reportGenerator;
    }

    public User registerUser(String username, String email, String password) {
        User user = new User(username, email, password);
        User saved = userRepository.save(user);
        emailService.sendWelcomeEmail(saved);
        return saved;
    }

    public byte[] getUserReport(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new UserNotFoundException("User not found: " + userId));
        return reportGenerator.generateUserReport(user);
    }
}

Notice that the orchestrating UserRegistrationService depends only on interfaces and domain objects — it does not know whether email goes through SendGrid or SMTP, or whether reports use iText or Apache PDFBox. Each collaborator can change independently.

Diagrams

Correct Class Structure

Responsibility Flow

Repository Pattern Internals

Violation Detection Flowchart

Before vs. After Structure

Unit Test — Easy Testing of Separated Concerns

One of the most immediate benefits of SRP is testability. Each class can now be tested in complete isolation.

java
@ExtendWith(MockitoExtension.class)
class UserRegistrationServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private EmailService emailService;

    @Mock
    private UserReportGenerator reportGenerator;

    @InjectMocks
    private UserRegistrationService registrationService;

    @Test
    void registerUser_savesUserAndSendsEmail() {
        // Arrange
        User savedUser = new User("alice", "alice@example.com", "hash");
        savedUser.setId(1L);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        // Act
        User result = registrationService.registerUser("alice", "alice@example.com", "hash");

        // Assert
        assertThat(result.getUsername()).isEqualTo("alice");
        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail(savedUser);
        // ReportGenerator was NOT called — correct boundary enforcement
        verifyNoInteractions(reportGenerator);
    }

    @Test
    void registerUser_emailFailure_doesNotAffectPersistence() {
        // Because EmailService is separate, we can verify isolation
        User savedUser = new User("bob", "bob@example.com", "hash");
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        doThrow(new MailException("SMTP down") {}).when(emailService).sendWelcomeEmail(any());

        // The service can choose to handle this separately — concerns are isolated
        assertThrows(MailException.class,
                () -> registrationService.registerUser("bob", "bob@example.com", "hash"));

        // But we can verify save still happened before the email failure
        verify(userRepository).save(any(User.class));
    }
}

With the anti-pattern God Class, none of these tests would be possible without spinning up a real SMTP server and a real database in the same test.

Best Practices

  1. Think in "reasons to change" — Ask yourself: "Who would ask me to change this class?" If the answer involves different teams, stakeholders, or systems (the DBA, the marketing team, the front-end team), those are separate responsibilities.
  2. Avoid God Classes — If a class has more than a handful of public methods spanning different concerns, it is a warning sign. Refactor early before the class accumulates years of technical debt.
  3. One package = one concern — Package structure can reinforce SRP. Group user/domain, user/repository, user/email, user/reporting into cohesive packages rather than letting everything live in a flat com.example namespace.
  4. Use interfaces to separate contracts — Define a UserRepository interface before writing JpaUserRepository. The interface belongs to the domain; the implementation belongs to the infrastructure layer.
  5. Watch your imports — A class that imports from javax.mail, com.itextpdf, java.sql, and org.springframework.data.jpa all at once is almost certainly violating SRP.
  6. Apply the newspaper metaphor — Imagine your class as a newspaper article. It should cover one story cohesively. If you find yourself adding unrelated paragraphs, create a new article (class).

Other SOLID Principles:

Further Reading:

  • Repository Pattern: how UserRepository separates persistence from domain logic.
  • Service Layer Pattern: how UserRegistrationService orchestrates multiple SRP-compliant services.
  • Cohesion and Coupling: the theoretical foundations behind SRP.
  • REST API Design: applying SRP to API controller design.