Appearance
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:
- The user's data model changes (new fields, validation rules).
- The database schema or ORM technology changes.
- The email provider or template changes.
- 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
- 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.
- 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.
- One package = one concern — Package structure can reinforce SRP. Group
user/domain,user/repository,user/email,user/reportinginto cohesive packages rather than letting everything live in a flatcom.examplenamespace. - Use interfaces to separate contracts — Define a
UserRepositoryinterface before writingJpaUserRepository. The interface belongs to the domain; the implementation belongs to the infrastructure layer. - Watch your imports — A class that imports from
javax.mail,com.itextpdf,java.sql, andorg.springframework.data.jpaall at once is almost certainly violating SRP. - 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).
Related Concepts
Other SOLID Principles:
- OCP — Open/Closed Principle: Once responsibilities are separated, you can extend each one without modification.
- LSP — Liskov Substitution Principle: SRP-compliant hierarchies are easier to substitute.
- ISP — Interface Segregation Principle: Closely related — ISP applies SRP thinking to interface design.
- DIP — Dependency Inversion Principle: Separated classes should communicate through abstractions, not concrete types.
Further Reading:
- Repository Pattern: how
UserRepositoryseparates persistence from domain logic. - Service Layer Pattern: how
UserRegistrationServiceorchestrates multiple SRP-compliant services. - Cohesion and Coupling: the theoretical foundations behind SRP.
- REST API Design: applying SRP to API controller design.