Loading learning content...
When Robert C. Martin first articulated the Single Responsibility Principle, he described it as "a class should have one, and only one, reason to change." This formulation, while insightful, left room for interpretation. What exactly is a "reason to change"?
Over years of teaching and consulting, Uncle Bob refined this definition to something far more precise and actionable:
"A module should be responsible to one, and only one, actor."
This actor-based formulation transforms SRP from an abstract guideline into a concrete, verifiable principle. It grounds the concept of responsibility in organizational reality—the people and groups who actually drive software changes.
By the end of this page, you will understand how to identify actors in your organization, map them to software modules, and use actor analysis to guide design decisions. This is the key skill that separates developers who truly apply SRP from those who merely pay it lip service.
In the context of SRP, an actor is any person, role, group, or department that might request changes to your software. Actors are the sources of requirements—the stakeholders whose needs shape the system's evolution.
Key characteristics of actors:
Examples of actors in a typical business application:
The critical insight:
Actors aren't always people—they're roles. The same person might wear multiple hats in a small company, but when they switch between "CFO thinking" and "Operations thinking," they're acting as different actors. The software should reflect these distinct domains, regardless of whether one person happens to occupy both roles.
To identify actors, ask: "Who would I need to consult if I changed this code?" and "Whose approval would be required before deploying this change?" The answers reveal the actors responsible for that code.
The connection between actors and "reasons to change" becomes clear when we examine how software changes actually happen in organizations:
Each actor represents a distinct "reason to change." When a class is responsible to multiple actors, it can be changed for multiple reasons—and those changes can conflict, interfere, or cause unintended side effects.
123456789101112131415161718192021222324252627282930313233343536373839
// This class is responsible to THREE different actorsclass Employee { private hourlyRate: number; private hoursWorked: number; private department: string; // Actor 1: CFO / Finance // Changes when: Pay calculation policies change, tax rules update, // bonus structures evolve calculatePay(): number { const basePay = this.hourlyRate * this.hoursWorked; const overtime = this.calculateOvertimePay(); const bonus = this.calculateQuarterlyBonus(); return basePay + overtime + bonus; } // Actor 2: COO / Operations // Changes when: Time tracking policies change, work scheduling // rules update, productivity metrics evolve reportHours(): HoursReport { return { regular: Math.min(this.hoursWorked, 40), overtime: Math.max(0, this.hoursWorked - 40), isCompliant: this.validateWorkSchedule(), }; } // Actor 3: CTO / Engineering (DBA specifically) // Changes when: Database schema changes, persistence layer // updates, data migration needs arise save(): void { const sql = ` INSERT INTO employees (hourly_rate, hours_worked, department) VALUES (${this.hourlyRate}, ${this.hoursWorked}, '${this.department}') `; database.execute(sql); }}The problems with multi-actor classes:
Merge Conflicts — Two developers, working for different actors, modify the same file simultaneously. Their changes conceptually don't overlap, but they're forced to coordinate.
Accidental Coupling — A change for one actor inadvertently affects code used by another actor. The developer making the change may not even know the other actor exists.
Test Fragility — Changes for one actor break tests for another. The test suite becomes a battleground where satisfying one actor's requirements damages another's.
Deployment Risk — Rolling back a problem for one actor requires rolling back all changes—including unrelated changes for other actors.
Uncle Bob frequently uses the Employee class to illustrate SRP violations. Let's examine a realistic scenario that demonstrates why actor separation matters:
The Scenario:
Imagine an Employee class with two methods: calculatePay() and reportHours(). Both methods need to compute regular hours worked, so a developer creates a shared helper method: regularHours().
class Employee {
calculatePay() → calls regularHours()
reportHours() → calls regularHours()
regularHours() → shared implementation
}
Now, the CFO's team requests a change to how pay is calculated—specifically, they want to include time before the official shift starts as regular hours (for salaried employees who arrive early). A developer modifies regularHours() to accommodate this.
The problem:
The Operations team's reportHours() method also uses regularHours(). Their reports are now wrong—showing hours that don't match official time tracking policies. Operations didn't request any changes, didn't know about the modification, and now has incorrect data flowing into their workforce management systems.
This is the core problem with SRP violations: changes intended for one actor accidentally affect another. The Finance team got what they wanted, but Operations is now seeing incorrect reports. Neither team did anything wrong—the code structure was wrong.
The SRP-compliant solution:
Separate the class into modules, each responsible to a single actor:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Data class - no behavior, just holds employee dataclass EmployeeData { constructor( public readonly id: string, public readonly hourlyRate: number, public readonly hoursWorked: number, public readonly department: string, ) {}} // Actor: CFO / Finance// Single responsibility: Calculate compensationclass PayCalculator { calculatePay(employee: EmployeeData): number { const regularHours = this.calculateRegularHours(employee); const overtimeHours = this.calculateOvertimeHours(employee); const basePay = employee.hourlyRate * regularHours; const overtimePay = employee.hourlyRate * 1.5 * overtimeHours; return basePay + overtimePay; } // Private - only used by calculatePay() // Can change according to Finance's needs without affecting anyone else private calculateRegularHours(employee: EmployeeData): number { // Finance's definition of regular hours // (may include early arrival for salaried employees) return Math.min(employee.hoursWorked, 40); } private calculateOvertimeHours(employee: EmployeeData): number { return Math.max(0, employee.hoursWorked - 40); }} // Actor: COO / Operations// Single responsibility: Report hours for workforce managementclass HourReporter { reportHours(employee: EmployeeData): HoursReport { return { regular: this.calculateRegularHours(employee), overtime: this.calculateOvertimeHours(employee), isCompliant: this.validateCompliance(employee), }; } // Private - only used by reportHours() // Can change according to Operations' needs without affecting anyone else private calculateRegularHours(employee: EmployeeData): number { // Operations' definition of regular hours // (strict adherence to official shift times) return Math.min(employee.hoursWorked, 40); } private calculateOvertimeHours(employee: EmployeeData): number { return Math.max(0, employee.hoursWorked - 40); } private validateCompliance(employee: EmployeeData): boolean { // Validate against labor regulations return employee.hoursWorked <= 60; }} // Actor: CTO / Engineering (DBA)// Single responsibility: Persist employee dataclass EmployeeRepository { save(employee: EmployeeData): void { // Database-specific implementation const sql = ` INSERT INTO employees (id, hourly_rate, hours_worked, department) VALUES (?, ?, ?, ?) `; this.database.execute(sql, [ employee.id, employee.hourlyRate, employee.hoursWorked, employee.department, ]); } findById(id: string): EmployeeData | null { // Query implementation }}Notice what changed:
calculateRegularHours() exists separately in PayCalculator and HourReporter—they can diverge independentlyPayCalculator; changes for Operations affect only HourReporterEmployeeData class is just a data transfer object—no behavior, no actorIsn't this code duplication?
Yes, there's apparent duplication in the calculateRegularHours() methods. But this is intentional and correct. The two methods look the same today, but they serve different actors with different reasons to change. Forcing them to share an implementation creates accidental coupling—the very thing SRP exists to prevent.
The Rule of Convergence:
If two pieces of code look the same but serve different actors, keep them separate. If they truly represent the same concept (same actor, same reason to change), then extract the shared logic. The key is understanding why they look the same—is it conceptual identity or mere coincidence?
Applying actor-based thinking requires a deliberate process. Here's a systematic approach to mapping actors to code:
| Code Area | Likely Actor | Change Triggers |
|---|---|---|
| Pricing calculations | Finance/Revenue | Promotion strategies, tax law changes, margin requirements |
| Product catalog display | Product/Merchandising | UX improvements, A/B tests, seasonal layouts |
| Checkout flow | Product/Conversion | Conversion optimization, payment options, guest checkout |
| Inventory management | Operations/Fulfillment | Warehouse changes, supplier integrations, stock policies |
| Payment processing | Finance/Treasury + Compliance | PCI requirements, new payment methods, fraud rules |
| User authentication | Security/InfoSec | Password policies, MFA requirements, session management |
| Order persistence | Engineering/Data | Schema migrations, performance optimization, backup policies |
| Analytics events | Data/Analytics | New metrics, attribution models, privacy regulations |
Your company's organizational structure often mirrors how responsibilities should be divided in code. If different departments own different aspects of a feature, those aspects should probably live in different classes. The software architecture often benefits from reflecting the organizational architecture.
Real-world actor mapping isn't always clean. Here are common edge cases and how to handle them:
Edge Case 1: Multiple People, Same Actor
In a large Finance department, dozens of people might request changes to payroll calculations. They're still one actor—they represent the same stakeholder concern (accurate compensation) and rarely disagree internally about requirements.
Resolution: Think of actors as roles, not individuals. If multiple people speak with one voice on a topic, they're one actor.
Edge Case 2: One Person, Multiple Actors
In a startup, the CEO might be the actor for marketing, finance, operations, and product decisions. They're still multiple actors—they switch between different modes of thinking.
Resolution: Consider the domain of expertise they're drawing on, not their job title. When they think about costs, they're acting as the Finance actor. When they think about user experience, they're Product.
Edge Case 3: Cross-Cutting Concerns
Logging, monitoring, and security touch every part of the system. Who is their actor?
Resolution: Cross-cutting concerns typically have dedicated actors: the Security team for authentication/authorization, the SRE team for observability, the Compliance team for audit logging. Use aspect-oriented techniques or decorators to separate these concerns without coupling your business logic classes to them.
Edge Case 4: Unclear Ownership
Some features straddle boundaries. A "customer profile page" involves Product (UX), Marketing (messaging), Privacy (consent management), and Engineering (performance).
Resolution: Decompose by the dominant concern. The UI composition might be Product-owned, while specific widgets (consent banner, performance metrics) are extracted to modules owned by their respective actors.
If you find that changes for one actor regularly break requirements for another, you have a design problem. Either the class has multiple responsibilities (split it), or the organization has conflicting requirements (escalate to product/management).
Actor-based thinking applies at every level of system design, not just classes:
Method Level:
Each method should serve a single actor's need. Private helper methods all support the same actor—they're implementation details of the public responsibility.
Class Level:
This is the primary SRP application. Each class serves one actor, encapsulating all logic that actor needs.
Module/Package Level:
Groups of closely related classes serving the same actor belong in the same module. The module boundary represents a cohesive unit of change.
Service Level (Microservices):
In distributed systems, each service should ideally serve one actor or a tightly coupled group of actors. This is called "service autonomy"—each team can develop, deploy, and scale their service independently.
System Level:
Entire systems or product lines serve distinct actor groups. A company's CRM and ERP systems serve different actors (Sales vs. Finance/Operations), even though they share data.
| Scale | Unit | Actor Alignment Example |
|---|---|---|
| Method | calculateEmployeePay() | Implements Finance's pay rules |
| Class | PayrollCalculator | Encapsulates all Finance pay logic |
| Module | payroll/ | Contains all payroll-related classes for Finance |
| Service | Payroll Service | Owned by Finance team, independent deployment |
| System | HR Suite | Serves HR and Finance departments |
The Fractal Nature of Responsibility:
Notice how responsibility scales fractally. A microservice has one responsibility at the system level, but contains classes each with their own responsibility at the class level. This nesting is natural—large responsibilities decompose into smaller, more focused ones.
The key principle at every level: One unit should answer to one authority. Whether that unit is a method, a class, a module, or a service, it should have a clear owner whose needs it exclusively serves.
The actor-based definition of SRP transforms it from a vague guideline into a precise, actionable principle. Let's consolidate the key insights:
What's next:
With actors identified, we need a way to determine if code truly belongs together. The next page explores cohesion—the measure of how strongly the elements within a module belong together. Cohesion provides the positive test for SRP: not just "are there multiple actors?" but "do all elements serve the same purpose?"
You now understand the actor-based formulation of SRP. This perspective—focusing on who causes change rather than what the code does—is the key to applying SRP effectively in real-world systems. Next, we'll explore cohesion as the positive measure of well-designed responsibilities.