Loading content...
When Robert C. Martin first articulated the Single Responsibility Principle, he defined it as 'a class should have only one reason to change.' This formulation, while powerful, left room for interpretation. What exactly constitutes a 'reason to change'? Different developers might identify different reasons in the same code, leading to inconsistent applications of the principle.
Over years of teaching and consulting, Uncle Bob refined this definition into something more precise and actionable. The evolved formulation connects SRP directly to the organizational and social forces that drive software change—specifically, to the actors who interact with and request changes to software systems.
This refined understanding transforms SRP from an abstract design guideline into a concrete analytical tool. By identifying actors and mapping code to stakeholder concerns, we can systematically detect SRP violations and design classes that genuinely embody single responsibilities.
In this page, we explore Uncle Bob's mature formulation of SRP, understand why actors provide a more reliable lens than abstract 'reasons,' and develop practical techniques for applying this insight in real-world design.
By the end of this page, you will understand Uncle Bob's actor-based SRP definition, why connecting responsibilities to stakeholders produces clearer designs, how to identify actors in your organization, and how to use actor analysis to detect and resolve SRP violations systematically.
Uncle Bob's refined articulation of SRP is:
A module should be responsible to one, and only one, actor.
Let's unpack each element of this statement:
Module
In this context, 'module' typically means a class, but it can extend to any cohesive unit of code—a source file, a package, a service. The principle scales. What matters is that we're talking about a coherent unit that groups related code.
Responsible to
This phrase captures the direction of the relationship. The module serves an actor. It implements functionality that the actor needs. When the actor's requirements change, the module changes to satisfy those new requirements.
One, and only one
The module should serve exactly one actor. Not two actors. Not 'primarily one actor with some features for another.' Exactly one. When you find a module serving multiple actors, you've found an SRP violation.
Actor
An actor is a person or group of people that represents a single source of change requests. Crucially, an actor is identified not by job title or individual identity, but by their role relative to the software:
If the same person wears multiple hats (both CFO and CTO), they still represent distinct actors when requesting different types of changes.
The shift from 'reasons to change' to 'actors' is profound. 'Reasons' are abstract and can be endlessly subdivided or combined. Actors are concrete—they're real people or teams in your organization. You can identify them, talk to them, and ask them about their requirements. This concreteness makes actor-based analysis more reliable and actionable than reasoning about abstract 'reasons.'
The Connection to Conway's Law
Uncle Bob's formulation connects SRP to Conway's Law, which states:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.
If your organization has separate teams for UI, business logic, and data management, your code modules should align with those teams. Each module should 'belong' to a single team—a single actor. This alignment:
Conversely, when a module serves multiple actors, it becomes a contention point—a shared resource that multiple teams must coordinate around. This slows development, creates merge conflicts, and increases the risk of unintended side effects.
To apply the actor-based formulation, we must understand what actors are and how to identify them.
Actors Are Roles, Not People
An actor is a role that represents a source of change, not a specific person. Consider these examples:
| Person | Role/Actor | Type of Change |
|---|---|---|
| Sarah (CFO) | Financial Reporting | Pay calculation rules, tax formulas |
| Sarah (CFO) | Executive Dashboards | KPI displays, report formats |
| Mike (Senior Dev) | Authentication System | Login flows, security policies |
| The Product Team | Feature Requirements | Business logic, workflows |
| The DevOps Team | Operational Concerns | Logging, monitoring, deployment |
Notice that Sarah appears twice—once as the 'Financial Reporting' actor and once as the 'Executive Dashboards' actor. Even though she's one person, she represents two distinct actors because she requests two different types of changes with different motivations.
Identifying Actors in Your Context
To identify actors relevant to a piece of code:
Actor Boundaries Reflect Change Patterns
The value of actor identification comes from recognizing that changes from different actors have different:
By separating code by actor, we separate code by these characteristics. Each module can have an appropriate testing strategy, review process, and deployment cadence.
In startups or small teams where one person wears many hats, actors still exist—they're just embodied in the same individual. The principle remains: separate the concerns even if the same person currently handles all of them. This prepares the codebase for team growth and clarifies thinking about what belongs together.
The actor-based formulation provides several advantages over the original 'reasons to change' framing:
1. Actors Are Observable
You can identify actors by observing who requests changes in your organization. Check your ticketing system—who files the requests? Check your pull request comments—who asks for modifications? These are your actors. Abstract 'reasons to change' require speculation; actors are empirically identifiable.
2. Actors Are Stable
Organizational roles tend to be more stable than technical concerns. 'The finance team' is a durable concept even as technologies evolve. 'Reasons to change' can shift based on how you conceptualize the code. Actors provide a stable reference point.
3. Actors Provide Clear Ownership
When each module is responsible to one actor, ownership is unambiguous. The payroll calculation module belongs to Accounting. The authentication module belongs to Security. This clarity accelerates decision-making and reduces coordination overhead.
4. Actors Reveal Hidden Coupling
When you map code to actors and find that a single class serves multiple actors, you've discovered coupling that might otherwise be invisible. This coupling would eventually cause problems—the actor lens surfaces it early.
Example: The Multi-Actor Problem
Consider a ReportService class that generates reports. At first glance, it seems focused—it just does 'reporting.' But applying actor analysis reveals complexity:
| Method | Purpose | Actor |
|---|---|---|
generateFinancialReport() | P&L statements | CFO/Finance |
generateOperationalReport() | Productivity metrics | COO/Operations |
generateComplianceReport() | Regulatory submissions | Legal/Compliance |
exportToExcel() | Download format | All actors |
Three distinct actors depend on this class. When the CFO requests new financial metrics, changes risk affecting compliance reports. When Legal requires additional audit fields, the modification might break operational dashboards.
The solution: separate into FinancialReportService, OperationalReportService, and ComplianceReportService. Each serves a single actor. If needed, an ExportService handles format conversions—it serves the technical concern of 'formatting,' which could be owned by a shared infrastructure actor.
Aligning code with actors doesn't mean mechanically copying your org chart into packages. Some organizational divisions are historical accidents, not conceptual boundaries. Use actor analysis as a lens for understanding change patterns, not as a template for code structure. The goal is stable modules with clear ownership, not bureaucratic mirroring.
Uncle Bob often illustrates SRP with an Employee class. Let's revisit this example through the actor lens:
The Problematic Employee Class
123456789101112131415161718192021222324252627
public class Employee { private String name; private double hourlyRate; // Method 1: Called by Accounting public double calculatePay() { // Complex pay calculation with overtime rules, // tax considerations, benefits deductions... return calculateRegularPay() + calculateOvertimePay(); } // Method 2: Called by HR public void reportHours(int hours) { // Track time for HR reporting, // validate against policies... } // Method 3: Called by IT/Persistence public void save() { // Write to database, handle transactions... } // Shared helper - used by which actor? private double regularHours() { // Returns hours up to 40 }}Actor Analysis
| Method | Actor | Why They Request Changes |
|---|---|---|
calculatePay() | Accounting/CFO | Tax law changes, new benefit types, overtime rules |
reportHours() | HR | Policy changes, new leave types, compliance reporting |
save() | IT/DBA | Schema migrations, database technology changes |
regularHours() | Conflict! | Used by both calculatePay() and reportHours() |
The Hidden Danger
The regularHours() method is shared between pay calculation and hour reporting. This creates a dangerous coupling:
regularHours() to implement the new overtime threshold.reportHours(), which HR relies on for compliance.This is the essence of SRP violation: changes requested by one actor inadvertently affect functionality relied upon by another actor.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Each class serves a single actor // Actor: Accounting/CFOpublic class PayCalculator { public Money calculatePay(Employee employee, TimePeriod period) { Hours regularHours = calculateRegularHoursForPay(employee, period); Hours overtimeHours = calculateOvertimeHoursForPay(employee, period); return calculateCompensation(employee, regularHours, overtimeHours); } private Hours calculateRegularHoursForPay(Employee e, TimePeriod p) { // Pay-specific calculation - can evolve independently }} // Actor: HRpublic class HourReporter { public HoursReport generateReport(Employee employee, TimePeriod period) { Hours regularHours = calculateRegularHoursForHR(employee, period); Hours overtimeHours = calculateOvertimeHoursForHR(employee, period); return new HoursReport(regularHours, overtimeHours); } private Hours calculateRegularHoursForHR(Employee e, TimePeriod p) { // HR-specific calculation - can evolve independently }} // Actor: IT/DBApublic class EmployeeRepository { public void save(Employee employee) { // Persistence logic } public Employee findById(EmployeeId id) { // Retrieval logic }} // Data carrier - minimal behaviorpublic class Employee { private final EmployeeId id; private final String name; private final Money hourlyRate; // ... getters only}In the refactored design, calculateRegularHoursForPay() and calculateRegularHoursForHR() might initially contain identical code. That's acceptable—and even preferable—because they represent different concepts that happen to have the same implementation today. As each actor's requirements evolve, these methods can diverge without affecting the other actor.
Armed with the actor model, we can develop systematic techniques for detecting SRP violations:
Technique 1: Stakeholder Mapping
For each public method in a class, ask:
If different methods map to different stakeholders, you may have an SRP violation.
Technique 2: Change History Analysis
Review the version control history:
If you see commits from multiple teams addressing unrelated concerns, the class likely serves multiple actors.
Technique 3: Dependency Clustering
Examine the class's dependencies:
Disparate dependencies often indicate disparate responsibilities.
Technique 4: The 'What If' Exercise
Imagine various change scenarios and trace their impact:
What if the tax rates change?
What if we switch database vendors?
What if the report format changes?
Each scenario tests whether the class is genuinely focused on a single actor's concerns.
During code reviews, ask the submitter: 'Which actor does this change serve?' If they can't give a clear, single answer, the change might be adding inappropriate responsibility to an existing class—or the class might already be serving too many actors. This question surfaces SRP concerns early.
Let's walk through a systematic process for applying the actor-based SRP:
Step 1: Enumerate Class Behaviors
List all public methods and any significant protected or private methods that represent distinct behaviors. Don't list trivial getters/setters—focus on methods that 'do something.'
Step 2: Map Behaviors to Actors
For each behavior, identify which actor would:
Step 3: Identify Actor Conflicts
Look for:
Step 4: Design the Separation
If conflicts exist:
Step 5: Validate Independence
Confirm that:
1234567891011121314151617
public class OrderProcessor { // Actor: Sales team - order workflow public Order createOrder(Customer customer, List<LineItem> items) { ... } public Order applyDiscount(Order order, DiscountCode code) { ... } // Actor: Fulfillment team - shipping public ShippingLabel generateShippingLabel(Order order) { ... } public void schedulePickup(Order order, LocalDate date) { ... } // Actor: Accounting - financial records public Invoice generateInvoice(Order order) { ... } public void recordRevenue(Order order) { ... } // Actor: Analytics - business intelligence public void trackOrderMetrics(Order order) { ... } public OrderStatistics getStatistics(DateRange range) { ... }}1234567891011121314151617181920212223242526272829303132
// Actor: Sales teampublic class OrderService { public Order createOrder(Customer customer, List<LineItem> items) { ... } public Order applyDiscount(Order order, DiscountCode code) { ... }} // Actor: Fulfillment teampublic class ShippingService { public ShippingLabel generateLabel(Order order) { ... } public void schedulePickup(Order order, LocalDate date) { ... }} // Actor: Accountingpublic class BillingService { public Invoice generateInvoice(Order order) { ... } public void recordRevenue(Order order) { ... }} // Actor: Analyticspublic class OrderAnalyticsService { public void trackMetrics(Order order) { ... } public OrderStatistics getStatistics(DateRange range) { ... }} // Data class - no single actor owns thispublic class Order { private final OrderId id; private final Customer customer; private final List<LineItem> items; private final OrderStatus status; // ... minimal behavior, primarily data access}Data classes (like Order in the example) often don't have a single actor—many actors care about order data. That's acceptable because data classes have minimal behavior. The SRP focuses on behavior—code that performs operations and makes decisions. Data structures that just carry information are less prone to the change-propagation problems SRP addresses.
Applying actor-based SRP isn't always straightforward. Here are common challenges and how to address them:
Challenge 1: Cross-Cutting Concerns
Logging, security, and monitoring seem to belong to everyone. Which actor owns them?
Solution: These are typically owned by an 'Infrastructure' or 'Operations' actor. Separate them into dedicated modules (logging framework, security service) that are used by—but not bundled into—business logic classes. Use aspects, decorators, or middleware for cross-cutting application.
Challenge 2: Overlapping Actor Interests
Sometimes two actors legitimately care about the same functionality. The CFO and the COO both care about revenue reporting.
Solution: Look deeper. Often, they care about different aspects. The CFO cares about accuracy for financial statements; the COO cares about real-time visibility for operations. These might be separate reports or separate views of the same data. If they truly share a concern, consider whether they're actually one actor for that concern.
Challenge 3: Unclear Organizational Boundaries
In organizations with fluid structures, actors may be hard to identify.
Solution: Focus on patterns of change, not org charts. Ask: 'When this code changes, why does it change?' The answer reveals actors even if they don't map to formal roles. 'Because the regulations changed' points to a compliance actor. 'Because customers requested a new feature' points to a product actor.
Challenge 4: Legacy Codebases
Existing classes already violate SRP and refactoring is expensive.
Solution: Apply SRP incrementally. When making changes:
Over time, the monolithic class shrinks as actor-specific functionality migrates out.
The actor model is a powerful lens, but don't apply it mechanically. If two methods serve the same actor and share significant implementation, they belong together even if you could theoretically separate them. Use judgment—the goal is maintainable code, not ideological purity.
Uncle Bob's evolution of SRP from 'reasons to change' to 'actors' provides a more precise and actionable framework. Let's consolidate the key insights:
What's Next
Having understood the definition and Uncle Bob's refined formulation, we're ready to explore why SRP holds such a central position among design principles. In the next page, we'll examine how SRP serves as the foundation of good design—enabling all other SOLID principles, creating testable code, and producing systems that remain flexible over years of evolution.
You now understand Uncle Bob's actor-based formulation of SRP. This concrete lens—focusing on who requests changes rather than abstract reasons—transforms SRP from a conceptual guideline into a practical analytical tool for designing cohesive, maintainable modules.