Loading learning content...
Every seasoned engineer has encountered it—the God Class. Perhaps it was called UserManager, ApplicationController, or simply Utils. It started as a reasonable 200-line class, but over years of feature additions, bug fixes, and 'just one more method,' it swelled into a 3,000-line monstrosity that half the team feared and the other half refused to touch.
This class became the bottleneck for everything. Every feature touched it. Every merge conflict involved it. Every regression traced back to it. The class that did everything became the class that broke everything.
The Single Responsibility Principle (SRP), when properly applied at the class level, is your primary weapon against this chaos. It transforms unwieldy code into focused, manageable components that evolve gracefully over time.
By the end of this page, you will understand how to identify a class's responsibility, recognize when a class has acquired multiple responsibilities, and refactor toward focused classes that are easier to maintain, test, and extend. You'll develop the intuition to design classes correctly from the start.
At the class level, SRP can be stated as:
A class should have one, and only one, reason to change.
But what exactly constitutes a 'reason to change'? Uncle Bob Martin, who popularized SRP, refined this definition over time to be more precise:
A class should have only one actor (stakeholder or source of change) that it serves.
This actor-based definition is crucial because 'responsibility' is fundamentally about who drives changes to the code, not just what the code does. Different actors have different concerns, different timelines, and different reasons for requesting modifications.
When evaluating whether a class follows SRP, ask: 'Who would request changes to this class?' If the answer involves multiple distinct stakeholders—the CFO for reporting logic, the CTO for technical requirements, the compliance team for legal rules—you likely have an SRP violation.
Understanding 'Actor' in Practice:
An 'actor' is not necessarily a specific person, but rather a role or group that has authority over certain requirements. Common actors include:
When a single class serves multiple actors, changes requested by one actor can inadvertently break functionality owned by another. This coupling creates fragility and reduces team autonomy.
1234567891011121314151617181920212223242526
// SRP VIOLATION: Multiple actors influence this classpublic class Employee { private String name; private double hourlyRate; private int hoursWorked; // Method for HR Department (Actor 1) public double calculatePay() { return hourlyRate * hoursWorked; } // Method for Accounting Department (Actor 2) public String generatePayrollReport() { return String.format("Employee: %s, Pay: $%.2f", name, calculatePay()); } // Method for IT/Persistence (Actor 3) public void saveToDatabase(Connection conn) { // SQL operations to persist employee } // Problem: Change to calculatePay() for HR // might break report format for Accounting // Database schema changes affect domain logic}In the example above, three distinct actors influence the Employee class:
When the CFO requests a change to how overtime is calculated, the developer must modify calculatePay(). But this method is also used by generatePayrollReport(). A subtle change in pay calculation could alter report output, potentially breaking downstream accounting processes that depend on specific formatting.
This is the essence of SRP violation—hidden coupling between unrelated concerns.
Cohesion is the degree to which the elements within a module (in this case, a class) belong together. A highly cohesive class has methods and data that are closely related, all working toward a single, well-defined purpose. A low-cohesion class is a grab-bag of loosely related or unrelated functionality.
Cohesion and SRP are deeply intertwined:
A class with a single responsibility naturally exhibits high cohesion.
Conversely, when you observe low cohesion—methods that don't use most of the class's fields, or fields that are only used by a subset of methods—you're often looking at an SRP violation waiting to be extracted.
| Cohesion Type | Description | SRP Alignment |
|---|---|---|
| Functional | All parts contribute to a single, well-defined task | Strong — Ideal for SRP |
| Sequential | Output of one part is input to another | Good — Often appropriate |
| Communicational | Methods operate on the same data | Moderate — Watch for splits |
| Procedural | Parts always execute in a certain order | Weak — Consider separation |
| Temporal | Parts are grouped by when they execute | Poor — Usually violates SRP |
| Logical | Parts are logically categorized together | Poor — 'Utility' class smell |
| Coincidental | Parts have no meaningful relationship | Worst — Definite violation |
Measuring Cohesion:
While formal metrics like LCOM (Lack of Cohesion of Methods) exist, practical cohesion assessment often relies on heuristics:
The Field-Method Matrix Test: Create a mental matrix where rows are methods and columns are instance fields. Mark which methods access which fields. In a highly cohesive class, most methods access most fields. If you see clear clusters—some methods only touch certain fields while others touch different fields—you likely have two responsibilities waiting to be separated.
123456789101112131415161718192021222324252627282930313233
// LOW COHESION: Methods cluster around different fieldspublic class UserService { // Cluster A: Authentication fields private PasswordEncoder encoder; private SessionManager sessions; // Cluster B: Profile fields private ProfileRepository profiles; private ImageResizer imageResizer; // Only uses Cluster A public boolean authenticate(String user, String pass) { // Uses encoder and sessions, never touches profiles or images } // Only uses Cluster A public void logout(String sessionId) { sessions.invalidate(sessionId); } // Only uses Cluster B public void updateAvatar(String userId, byte[] image) { // Uses profiles and imageResizer, never touches auth } // Only uses Cluster B public Profile getProfile(String userId) { return profiles.findById(userId); } // INSIGHT: Two classes hiding inside one // AuthenticationService + ProfileService}If you find yourself prefixing method names to create logical groupings within a class (authLogin, authLogout, profileUpdate, profileGet), you're documenting an SRP violation. Those prefixes are the names of the classes that should exist.
SRP violations rarely announce themselves. They accumulate gradually as features are added, and by the time they're obvious, significant refactoring is required. Learning to recognize early warning signs is essential for maintaining healthy codebases.
The Modification History Test:
Examine your version control history. For a given class, look at the last 10 commits that modified it:
If you see patterns like 'fix payment calculation,' 'update email template,' 'add audit logging,' and 'change database timeout' all affecting the same class, you're looking at an SRP violation manifested through history.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// THE GOD CLASS: Maximum SRP violationpublic class OrderProcessor { // Dependencies reveal scattered responsibilities private DatabaseConnection db; private EmailService email; private PaymentGateway payments; private InventoryService inventory; private ShippingCalculator shipping; private TaxCalculator tax; private FraudDetector fraud; private AuditLogger audit; private NotificationHub notifications; private ReportGenerator reports; public Order processOrder(Cart cart, Customer customer) { // Responsibility 1: Validation validateCart(cart); // Responsibility 2: Fraud detection if (fraud.isSuspicious(customer, cart)) { flagForReview(cart); return null; } // Responsibility 3: Pricing/Tax calculation double subtotal = calculateSubtotal(cart); double taxAmount = tax.calculate(subtotal, customer.getAddress()); double shippingCost = shipping.calculate(cart, customer.getAddress()); // Responsibility 4: Payment processing PaymentResult result = payments.charge(customer, subtotal + taxAmount + shippingCost); // Responsibility 5: Inventory management inventory.reserve(cart.getItems()); // Responsibility 6: Order persistence Order order = createOrder(cart, customer, result); db.save(order); // Responsibility 7: Notifications email.sendConfirmation(customer, order); notifications.push(customer.getId(), "Order confirmed!"); // Responsibility 8: Audit audit.log("ORDER_CREATED", order.getId()); // Responsibility 9: Analytics/Reporting reports.recordSale(order); return order; } // 50+ more methods mixing all these concerns...}This class is a nexus of coupling. Any change to payment processing, email formatting, tax rules, fraud detection, or a dozen other concerns forces modification to this single file. Multiple developers working on unrelated features will constantly conflict in version control. Testing requires mocking 10 dependencies. This is the class everyone dreads.
Transforming an SRP-violating class into properly focused components is a surgical process. The goal is to identify responsibility boundaries and extract cohesive units while maintaining existing behavior. Here's a systematic approach:
Applying the Process:
Let's refactor the Employee class we saw earlier. We identified three actors: HR (pay calculation), Accounting (reporting), and IT (persistence).
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
// STEP 1: Core domain class - only holds data and core identitypublic class Employee { private final String id; private String name; private double hourlyRate; private int hoursWorked; // Simple getters and setters - domain data only public String getId() { return id; } public String getName() { return name; } public double getHourlyRate() { return hourlyRate; } public int getHoursWorked() { return hoursWorked; } // Note: No calculation, no persistence, no formatting} // STEP 2: Pay Calculator - serves HR actorpublic class PayCalculator { public double calculateRegularPay(Employee employee) { return employee.getHourlyRate() * employee.getHoursWorked(); } public double calculateOvertime(Employee employee, int regularHours) { int overtimeHours = Math.max(0, employee.getHoursWorked() - regularHours); return overtimeHours * employee.getHourlyRate() * 1.5; } public double calculateTotalPay(Employee employee) { return calculateRegularPay(employee) + calculateOvertime(employee, 40); }} // STEP 3: Report Generator - serves Accounting actorpublic class PayrollReportGenerator { private final PayCalculator payCalculator; public PayrollReportGenerator(PayCalculator payCalculator) { this.payCalculator = payCalculator; } public String generatePayrollReport(Employee employee) { double pay = payCalculator.calculateTotalPay(employee); return String.format("Employee: %s\nID: %s\nTotal Pay: $%.2f", employee.getName(), employee.getId(), pay); } public String generateDepartmentSummary(List<Employee> employees) { // Accounting-specific report formatting }} // STEP 4: Repository - serves IT/DBA actorpublic class EmployeeRepository { private final DataSource dataSource; public void save(Employee employee) { // Pure persistence logic - isolated from domain } public Employee findById(String id) { // Pure persistence logic } public List<Employee> findByDepartment(String department) { // Query logic isolated from business rules }}Each class now has a single reason to change. HR can modify pay calculations without affecting reports. Accounting can change report formats without risking pay logic. DBAs can optimize queries without touching business rules. Teams can work independently, and testing becomes trivial—no complex mocking required.
While SRP violations cause significant problems, the opposite extreme—excessive decomposition—creates its own challenges. Taken to an absurd degree, SRP could justify a separate class for every method, leading to an explosion of tiny classes that are individually simple but collectively incomprehensible.
Finding the Right Granularity:
The appropriate level of decomposition depends on several factors:
Rate of change: If two pieces of functionality always change together and are owned by the same actor, keeping them together avoids unnecessary indirection.
Cognitive load: A class should be comprehensible as a unit. If reading a class requires simultaneously holding 5 other classes in mind, you've gone too far.
Team structure: Conway's Law applies—system structure mirrors organizational structure. If one team owns both payment and refund logic, a single PaymentService might be appropriate. If separate teams own each, separate classes make sense.
Reuse potential: Functionality likely to be reused across different contexts benefits from extraction. Functionality that only exists in one context may not.
EmailValidatorEmailFormatterEmailAddressParserEmailDomainCheckerEmailLocalPartValidatorEmailAddress (value object)localPart() and domain()Ask yourself: 'Would these ever change independently?' and 'Does separating these help or hurt understanding?' If two pieces always change together and combining them aids comprehension, keep them together. SRP is about managing change, not minimizing class size.
One of the most practical benefits of SRP-compliant classes is dramatically improved testability. The relationship is almost definitional: a class with a single responsibility is easy to test because it has a single thing to verify.
The testing benefits compound:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// TESTING SRP-VIOLATING CLASS: Complex, fragile testsclass OrderProcessorTest { @Mock DatabaseConnection db; @Mock EmailService email; @Mock PaymentGateway payments; @Mock InventoryService inventory; @Mock ShippingCalculator shipping; @Mock TaxCalculator tax; @Mock FraudDetector fraud; @Mock AuditLogger audit; @Mock NotificationHub notifications; @Mock ReportGenerator reports; @Test void processOrder_happyPath() { // 30 lines of mock setup... when(fraud.isSuspicious(any(), any())).thenReturn(false); when(payments.charge(any(), anyDouble())).thenReturn(success); when(shipping.calculate(any(), any())).thenReturn(10.0); when(tax.calculate(anyDouble(), any())).thenReturn(5.0); // ... more mocking ... Order result = processor.processOrder(cart, customer); // What are we even testing? All 9 responsibilities? // If this fails, which responsibility broke? }} // TESTING SRP-COMPLIANT CLASS: Simple, focused testsclass PayCalculatorTest { private PayCalculator calculator = new PayCalculator(); @Test void calculateTotalPay_withOvertime_appliesMultiplier() { Employee employee = new Employee("1", "Jane", 20.0, 50); double pay = calculator.calculateTotalPay(employee); // 40 regular hours × $20 = $800 // 10 overtime hours × $20 × 1.5 = $300 // Total = $1100 assertEquals(1100.0, pay); } @Test void calculateTotalPay_noOvertime_regularRateOnly() { Employee employee = new Employee("2", "John", 25.0, 35); double pay = calculator.calculateTotalPay(employee); assertEquals(875.0, pay); // 35 × $25 } // Tests are clear, fast, and test exactly one thing}If your test setup requires mocking more than 2-3 dependencies, step back and ask whether the class under test violates SRP. Excessive mocking is a symptom, not a normal cost of testing. Well-designed classes rarely need complex mock orchestration.
Let's examine SRP application in common scenarios from production systems.
Before: Typical UserService violation
12345678910111213141516171819
public class UserService { // Responsibility 1: User CRUD public User createUser(CreateUserRequest req) { ... } public User getUser(String id) { ... } public void updateUser(String id, UpdateRequest req) { ... } // Responsibility 2: Authentication public AuthToken login(String username, String password) { ... } public void logout(String token) { ... } public boolean validateToken(String token) { ... } // Responsibility 3: Password management public void changePassword(String userId, String old, String new) { ... } public void resetPassword(String email) { ... } // Responsibility 4: Profile management public void updateAvatar(String userId, byte[] image) { ... } public void updatePreferences(String userId, Preferences prefs) { ... }}After: SRP-compliant separation
12345
// Each class has ONE actorpublic class UserRepository { ... } // IT/DBA: CRUD operationspublic class AuthenticationService { ... } // Security: Login/logoutpublic class PasswordService { ... } // Security: Password policiespublic class ProfileService { ... } // Product/UX: Profile featuresClass-level SRP is the foundation upon which all other applications of the principle rest. Master it here, and the extension to methods, modules, and architecture becomes natural.
What's Next:
The class level is just the beginning. In the next page, we'll zoom in even further to examine SRP at the method level—how individual functions and methods should also embody single responsibility, and how this microscopic application of the principle affects code readability, reusability, and testability.
You now understand how SRP applies at the class level—the most common and foundational application of the principle. You can identify violations, understand their costs, apply systematic refactoring, and balance separation against over-engineering. Next, we'll explore SRP at the method level.