What is Single Responsibility Principle and How to Apply It
The Single Responsibility Principle (SRP) is the first principle of the SOLID acronym, formulated by Robert C. Martin ("Uncle Bob"):
🟢 Junior Level
The Single Responsibility Principle (SRP) is the first principle of the SOLID acronym, formulated by Robert C. Martin (“Uncle Bob”):
“A class should have one, and only one, reason to change.”
A common misconception is that a “reason to change” means “a class should perform only one action or have only one method.” In reality, a reason to change is tied directly to a business stakeholder or user role (Actor).
SRP Violation (Kitchen Sink Anti-Pattern):
+-----------------------------------------------+
| Employee |
| + calculateSalary() (Accounting / CFO) |
| + saveToDatabase() (DBA / Persistence) |
| + generateReport() (HR / Operations) |
+-----------------------------------------------+
(Three distinct departments force changes on the same file!)
SRP Compliance (Separation of Concerns):
+--------------+ +------------------+ +--------------------+ +--------------------+
| Employee | | SalaryCalculator | | EmployeeRepository | | EmployeeReporter |
| (Domain data | | (Payroll logic) | | (Database queries) | | (PDF / CSV format) |
| and state) | | | | | | |
+--------------+ +------------------+ +--------------------+ +--------------------+
Real-World Analogy
Consider a Swiss Army knife: it combines a blade, a corkscrew, scissors, and a saw. While convenient for camping, a professional restaurant kitchen never uses a pocketknife. A chef uses a dedicated chef’s knife, while a sommelier uses a dedicated corkscrew. If the corkscrew on a pocketknife snaps, the entire multi-tool must be sent away for repair, leaving the chef with no knife or scissors.
Core Misconception vs. Reality
- ❌ “A class must do exactly one thing and contain only one method.”
- ✅ “A class must be responsible to only one business actor and encapsulate one cohesive responsibility.” A class can contain ten public methods, provided all of them serve the same focused domain concept (for instance, a
Moneyvalue object containingadd(),subtract(),multiply(),allocate(), andisZero()).
// ❌ Violation: Employee mixes business logic, persistence, and formatting
public class Employee {
private Long id;
private String name;
private BigDecimal hourlyRate;
public BigDecimal calculatePay(int hoursWorked) {
return hourlyRate.multiply(BigDecimal.valueOf(hoursWorked));
}
public void saveToDatabase(DataSource dataSource) {
// Direct JDBC / SQL logic
}
public String generateHtmlReport() {
return "<div>" + name + "</div>";
}
}
// ✅ Compliant: Separated by actor and responsibility
public record Employee(Long id, String name, BigDecimal hourlyRate) {}
public class PayrollCalculator {
public BigDecimal calculateGrossPay(Employee employee, int hoursWorked) {
return employee.hourlyRate().multiply(BigDecimal.valueOf(hoursWorked));
}
}
public interface EmployeeRepository {
void save(Employee employee);
Optional<Employee> findById(Long id);
}
public class EmployeeHtmlReporter {
public String renderProfile(Employee employee) {
return "<div><h1>" + employee.name() + "</h1></div>";
}
}
🟡 Middle Level
1. Identifying SRP Violations
In modern Spring Boot applications, SRP violations often appear in overloaded @Service classes. Look for these four primary indicators:
| Symptom | Detection Signal | Why It Violates SRP |
|---|---|---|
| The “AND” Rule | Describing the class requires the conjunction “AND” (“It validates input AND charges the card AND writes SQL AND sends emails”). | The class spans multiple unrelated business workflows. |
| Multiple Stakeholders | Modifying VAT tax, migrating from Postgres to MongoDB, or altering email templates all touch the same service. | Multiple independent business actors drive changes to one file. |
| Constructor Bloat | A class constructor injects 8–15 dependencies (@Autowired private final Repositories / Clients). |
High fan-out coupling; the class knows too much about the system. |
| Testing Friction | Writing a unit test for one small calculation requires configuring 40 lines of mockito stubs (when(...).thenReturn(...)). |
Unit testing becomes integration testing due to excessive coupling. |
2. Refactoring a God Service to SRP
// ❌ BEFORE: Monolithic OrderService doing everything
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final JavaMailSender mailSender;
private final MeterRegistry meterRegistry;
@Transactional
public void processOrder(Order order) {
// 1. Validation
if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Invalid order amount");
}
// 2. Payment processing
paymentGateway.charge(order.getCustomerId(), order.getTotal());
// 3. Persistence
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
// 4. Email notification
SimpleMailMessage msg = new SimpleMailMessage();
msg.setTo(order.getCustomerEmail());
msg.setText("Your order has been paid successfully.");
mailSender.send(msg);
// 5. Metrics tracking
meterRegistry.counter("orders.completed").increment();
}
}
// ✅ AFTER: Orchestrator delegating to specialized components
@Service
@RequiredArgsConstructor
public class OrderProcessingCoordinator {
private final OrderValidator validator;
private final PaymentProcessor paymentProcessor;
private final OrderRepository orderRepository;
private final OrderNotificationService notifier;
@Transactional
public void processOrder(Order order) {
validator.validate(order);
paymentProcessor.charge(order);
order.markPaid();
orderRepository.save(order);
notifier.sendOrderConfirmation(order);
}
}
In the refactored design, OrderProcessingCoordinator has exactly one responsibility: orchestrating the checkout workflow steps. If payment APIs change, only PaymentProcessor changes. If email formatting changes, only OrderNotificationService changes.
🔴 Senior Level
1. High Cohesion vs. Low Coupling
SRP is the fundamental driver of High Cohesion (internal alignment) and Low Coupling (minimal external entanglement):
Low Cohesion (Anti-Pattern):
+-----------------------------------------+
| AppManager |
| [calculateTax] [sendSms] | (Methods share no private fields;
| [parseXml] [resizeImage] | unrelated concerns mashed together)
+-----------------------------------------+
High Cohesion (SRP Ideal):
+-----------------------------------------+
| TaxCalculator |
| - countryTaxRates: Map<Country, Rate> | (All methods operate on the same state
| + calculateVat() | to achieve one single business goal)
| + applyExemption() |
| + calculateNetTotal() |
+-----------------------------------------+
Cohesion Levels (from worst to best):
- Coincidental: Arbitrary grouping (
CommonUtils,GeneralHelper). - Logical: Methods grouped by category but functionally disparate (e.g., input validation for 20 unrelated forms).
- Temporal: Methods grouped because they happen at the same time (e.g., startup initialization of database, logging, and caches in one method).
- Procedural: Execution steps following an algorithm without shared state.
- Communicational: Methods operating on the same input data set.
- Sequential: Output of one method becomes input to the next.
- Functional (Target): Every element contributes to a single, well-defined domain task.
2. Mathematical Metric: LCOM (Lack of Cohesion in Methods)
In static analysis (SonarQube, Checkstyle), SRP adherence is quantified using the LCOM metric family:
- Let a class have $M$ methods ${M_1, M_2, \dots, M_m}$ and $F$ fields.
- Let $I_j$ be the set of fields accessed by method $M_j$.
- Consider all unordered pairs of methods $(M_i, M_j)$.
- Let $P = { (M_i, M_j) \mid I_i \cap I_j = \emptyset }$ (method pairs sharing no common fields).
- Let $Q = { (M_i, M_j) \mid I_i \cap I_j \neq \emptyset }$ (method pairs sharing at least one common field).
When $\text{LCOM1} > 0$, the class contains subsets of methods operating on completely disconnected fields. This mathematical indicator signals that the class should be split into multiple smaller, cohesive classes.
3. Conway’s Law and SRP Actors
Robert C. Martin refined the definition of SRP:
“A module should be responsible to one, and only one, actor.”
An Actor represents a single stakeholder, department, or user persona sharing identical requirements.
This directly reflects Conway’s Law: “Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.”
When an engineer merges financial compensation calculation and HR time-sheet tracking into a single EmployeeService:
- Two independent business departments demand concurrent modifications.
- Two separate engineering feature branches touch the same source file.
- Git merge conflicts arise, leading to cross-departmental regression bugs in production.
4 Tricky Questions
1. Does an orchestrator service that calls 10 downstream services violate SRP?
Answer: No, provided it is a pure Workflow Orchestrator (or Facade). Its single responsibility is coordinating the high-level business sequence (step order, transaction boundaries, rollback compensation).
It does not violate SRP if:
- It contains zero low-level implementation details (no raw SQL queries, no HTTP socket calls, no JSON payload parsing).
- It changes for only one reason: when the Product Owner alters the business sequence of that specific workflow.
2. What is “Class Explosion” and when does dogmatic adherence to SRP become harmful?
Answer:
Class Explosion (also known as “Atomic Dust”) occurs when developers decompose code down to single-method classes indiscriminately: OrderValidator, OrderTotalCalculator, OrderPriceRounder, and OrderDiscountApplier each consisting of 3 lines of code.
Harmful consequences:
- Cognitive overload: Understanding a simple 10-line business calculation requires navigating across 15 separate files and tabs in the IDE.
- Anemic Domain Model: State and behavior become decoupled, turning entities into plain getters/setters while procedural logic is scattered across tiny services.
- Metaspace pressure: Excessive classloader overhead from thousands of tiny classes.
Balance: Group closely related operations inside a Rich Domain Model or Domain Aggregate, packaging by business feature rather than technical micro-actions.
3. Why does the traditional 3-tier architecture (Controller-Service-Repository) encourage SRP violations?
Answer:
The layered architecture groups code by technical layer rather than business bounded contexts. Developers naturally create a single UserService or OrderService and continuously dump every method mentioning that entity into it (registration, password reset, avatar upload, KYC verification, CSV export, loyalty points).
Because the @Service layer imposes no internal boundaries, these services routinely grow into God Objects spanning thousands of lines.
Solution: Adopt Vertical Slice Architecture or CQRS command handlers. Instead of a monolithic UserService, split responsibilities into discrete Use Case Handlers: RegisterUserHandler, ResetPasswordHandler, and ExportUsersHandler.
4. Does the Active Record pattern violate SRP compared to Data Mapper / Repository?
Answer: Yes, architecturally it violates SRP. In the Active Record pattern (common in Ruby on Rails and Quarkus Panache), a single entity class combines two orthogonal responsibilities:
- Domain state and business rules (
user.calculateDiscount()). - Persistence and SQL database operations (
user.save(),User.findByName(...)).
If the database schema or indexing strategy changes, the domain model must be modified.
In enterprise systems, the Data Mapper / Repository pattern (Spring Data JPA) is preferred because it separates the pure domain model (User) from the persistence mechanism (UserRepository), preserving SRP.
🎯 Interview Cheat Sheet
Core Concepts:
- SRP Definition: A class should have one, and only one, reason to change (one business actor).
- Not One Method: SRP does not mandate single-method classes; it mandates focused domain cohesion.
- High Cohesion, Low Coupling: Methods within a class should operate on common state to serve a unified business purpose.
- Mathematical Signal: A high LCOM score indicates fragmented fields and methods ripe for decomposition.
- Conway’s Law Alignment: Aligning classes with business actors minimizes cross-team Git merge conflicts and deployment risks.
- Modern Pattern: Replace monolithic
@Serviceclasses with Vertical Slice Architecture (individual Use Case Handlers).
Red Flags to Avoid:
- ❌ “SRP means every Java class must only contain exactly one public method.” (Demonstrates misunderstanding of cohesion vs. granularity).
- ❌ “Creating
CommonUtilsorGeneralHelperclasses is good SRP practice.” (These are anti-patterns representing coincidental cohesion). - ❌ “Always decompose every operation into tiny classes, regardless of size.” (Leads to Class Explosion and an Anemic Domain Model).