Single Responsibility Principle (SRP)
The Single Responsibility Principle is the most important and most misunderstood of the SOLID principles.
Many developers think it means "a class should do one thing." That's not quite right. Robert C. Martin's actual definition is:
"A class should have only one reason to change."
A "reason to change" comes from a stakeholder—someone who might request modifications. If different people or departments could request different types of changes to your class, it has multiple responsibilities.
The Problem
Consider this class:
class Employee {
calculatePay(): number {
// Payroll rules
}
reportHours(): string {
// HR reporting format
}
saveToDatabase(): void {
// Database persistence
}
}
Who might request changes to this class?
- CFO/Accounting: "Change how overtime is calculated"
- HR: "Change the hours report format"
- DBA/IT: "Change from Postgres to MongoDB"
Three different stakeholders, three reasons to change. That's three responsibilities in one class.
Why This Matters
Unwanted Coupling
When accounting requests a pay calculation change, you modify Employee. But now HR's tests might break. IT's deployment is affected. The change scope is larger than it should be.
Merge Conflicts
When two teams modify the same class for different reasons, you get merge conflicts and coordination overhead.
Cognitive Load
A class with multiple responsibilities is harder to understand. You have to load all the context into your head, even if you only care about one aspect.
The Solution: Separate by Stakeholder
Split the class by who might request changes:
// Accounting owns this
class PayrollCalculator {
calculatePay(employee: Employee): number {
// Payroll rules
}
}
// HR owns this
class HoursReporter {
reportHours(employee: Employee): string {
// HR reporting format
}
}
// IT/Infrastructure owns this
class EmployeeRepository {
save(employee: Employee): void {
// Database persistence
}
find(id: string): Employee {
// Database query
}
}
// Just data, no behavior to change
class Employee {
id: string;
name: string;
hoursWorked: number;
hourlyRate: number;
}
Now:
- Accounting changes only affect
PayrollCalculator - HR changes only affect
HoursReporter - Database changes only affect
EmployeeRepository
Identifying Responsibilities
Ask: "Who would request a change to this code?"
Signs of Multiple Responsibilities
- Multiple
importgroups — Importing both UI libraries and database libraries suggests mixed concerns - Methods that don't use each other —
calculatePay()andsaveToDatabase()are unrelated - Different reasons to test — Some tests are about business rules, others about infrastructure
- The class keeps growing — Every new feature adds another method
The "And" Test
Describe what the class does. If you say "and," it might have multiple responsibilities:
- "Handles user authentication and profile management" → Split
- "Calculates order totals and generates invoices" → Split
- "Parses JSON" → One responsibility, fine
Common Violations
God Objects
class UserManager {
createUser() { }
authenticateUser() { }
updateProfile() { }
sendWelcomeEmail() { }
generateReport() { }
syncWithCRM() { }
backupUserData() { }
}
This "Manager" does everything related to users. It should be split into UserRepository, AuthService, ProfileService, UserNotifier, etc.
UI + Business Logic
class OrderForm {
validateInput() { } // UI validation
calculateTotal() { } // Business logic
renderConfirmation() { } // UI rendering
saveOrder() { } // Persistence
}
Mix of UI, business rules, and persistence.
Data + Behavior
class User {
name: string;
email: string;
sendEmail(subject: string, body: string) {
// Actually sends email
}
}
Should User know how to send emails? Or should an EmailService handle that?
Practical Application
Start with Cohesion
Methods that use the same data belong together. If calculatePay() uses hourlyRate and hoursWorked, but sendEmail() uses email, they have different cohesion.
Group by Actor
Identify who uses each method. Group methods by actor.
Extract When It Hurts
Don't preemptively split everything. When you feel the pain of multiple responsibilities—merge conflicts, unexpected test failures, hard-to-understand classes—that's when you split.
The Trade-off
SRP creates more classes. More files. More indirection.
Is this always better? No.
For a simple CRUD app, a UserController that handles all user operations might be fine. The stakeholder is just "the dev team," and the codebase is small.
But as the system grows, as teams specialize, as business rules get complex—SRP becomes essential.
Testing Connection
SRP makes testing easier:
PayrollCalculatorcan be tested with unit tests against payroll rulesEmployeeRepositorycan be tested against a test database- No need to mock unrelated dependencies
When a class is hard to test because it requires many mocks, it probably has multiple responsibilities.
Key insight: SRP isn't about "doing one thing"—it's about having one reason to change. Identify who might request changes to your code. If different stakeholders could request different changes, split the class. This reduces coupling, prevents merge conflicts, and makes code easier to understand.