A module should be responsible to one, and only one, actor. Robert Martin's original definition: 'A class should have only one reason to change.' Clean Architecture (2017) refined this: 'A module should be responsible to one, and only one, actor' - where 'actor' represents a group of stakeholders who would request changes for the same reason. Example: UserController (HTTP actor), UserService (business rules actor), UserRepository (database actor) - three distinct actors. Violation: UserManager serves CFO (reporting), COO (business rules), CTO (infrastructure) - changes for one actor break another's needs. Benefits: Changes isolated to one stakeholder group, easier testing, clearer ownership. Historical note: 'Reason to change' caused confusion - actor-based definition is more precise and prevents unexpected side effects when one group's requirements change.
Solid Single Responsibility FAQ & Answers
50 expert Solid Single Responsibility answers researched from official documentation. Every answer cites authoritative sources you can verify.
unknown
50 questionsAsk: 'Does this class serve more than one actor?' or 'Does it have multiple reasons to change?' Red flags: (1) Class name with 'Manager', 'Utils', 'Helper', 'And', 'Or' - indicates unclear responsibility. (2) God class (>300 lines, 15+ methods). (3) Methods dealing with unrelated concerns (sendEmail + calculateTax). (4) Multiple import groups (HTTP + database + email libs). (5) Testing requires mocking 5+ dependencies. (6) Hard to name in one sentence without 'and'. Example: class Invoice {calculate(), formatForPrint(), saveToDB()} serves three actors: accounting (calculation), printing team (format), DBAs (schema). Refactor: InvoiceCalculator, InvoicePrinter, InvoiceRepository. Detection tools: SonarQube, ReSharper, Visual Studio Code Analysis flag high complexity and low cohesion. Warning sign: Everyone adds features to this class because it's 'easy to access' - leads to monster God class over time.
SRP is class-level (each class one actor), Separation of Concerns (SoC) is system-level (architecture organized by concern). Robert Martin: 'Reasons for change are people' - SRP is about organizational actors who request changes. SRP is Martin's practical refinement of the broader SoC principle, specifically focused on actors and their reasons for requesting changes. SoC (Dijkstra): Broad principle dividing system into distinct concerns (technical, architectural, or organizational). Example: Frontend ↔ Backend API ↔ Database (SoC at system level). SRP within SoC: Backend has UserController (HTTP actor), UserService (business actor), UserRepository (data actor) - each following SRP. Violation: Backend separates layers but UserService handles validation + business + caching (multiple actors). SRP asks 'which actor owns this change?' (people-based), SoC asks 'what is the concern?' (feature-based). Clean Architecture applies both: SoC for layers (presentation, business, data), SRP within each layer. TypeScript example: class EmployeeRepository (DBA actor) vs class EmployeeReportFormatter (CFO actor) vs class EmployeeSalaryCalculator (accounting actor) - three actors, three classes.
Functions should do one thing at one abstraction level. SRP for functions: Each performs one cohesive task. Good: function validateEmail(email) {return /regex/.test(email);} - one task, clear name. Bad: function processUser(user) {validateEmail(user.email); hashPassword(user.password); saveToDatabase(user); sendWelcomeEmail(user);} - four responsibilities at different abstraction levels. Refactor: Create four single-responsibility functions, processUser() orchestrates. Violations: (1) Name with 'and' (validateAndSave), (2) Multiple return types (data | error | null), (3) Vague name (handleUserStuff). Robert Martin: 'Functions should be small and do one thing.' Test: Describe in one sentence without 'and'? If no, split it. SRP applies recursively: system → modules → classes → methods → functions. Each level has single responsibility at its scope. Modern: React functional components follow this - small focused components, custom hooks extract reusable logic.
Violation 1: class User {login(), logout(), updateProfile(), sendEmail(), generateReport()} - mixes auth (IT actor), profiles (users actor), email (ops actor), reporting (business actor). Fix: AuthService, UserProfileService, EmailService, ReportGenerator - four actors, four classes. Violation 2: class OrderController {validateOrder(), calculateTax(), processPayment(), updateInventory(), sendConfirmation()} - handles HTTP, validation, tax rules, payment, inventory, notifications. Fix: OrderController (HTTP only), OrderValidator, TaxCalculator, PaymentProcessor, InventoryManager, NotificationService. Violation 3 (E-commerce): One class fetches data from API, processes it, saves to database, sends email. Fix: OrderAPIClient, OrderProcessor, OrderRepository, OrderNotifier. Mobile app: Split data fetching (network actor) from UI processing (UX actor) using UserDataProcessor and UIController. React: Component handles fetch + filter + display. Fix: useFetchData() hook, useFilteredData() hook, DisplayComponent. Result: Each actor's changes isolated.
SRP is crucial for testability - enables unit testing in isolation with mocked dependencies. Connection: To test class, identify and inject dependencies. To identify dependency = identify responsibility. Single responsibility = minimal inputs/outputs/decision points = easy tests. Pattern: class OrderService {constructor(private paymentProcessor: PaymentProcessor, private inventory: InventoryManager)} - two clear dependencies, easy to mock. Testing SRP-compliant code: Mock dependencies, verify they're called correctly, test single concern. Example: Test OrderService processes payment via PaymentProcessor without needing real payment gateway or database. Violation consequences: class OrderService handles payment + inventory + email → testing requires mocking 10+ dependencies, brittle tests, hard to isolate failures. TDD benefits: Test-Driven Development forces identifying responsibilities upfront - if class hard to test, likely violates SRP. Dependency injection enables both SRP and testability: inject PaymentProcessor interface, vary behavior without changing OrderService. Tools: Use DI frameworks (NestJS, InversifyJS) to manage dependencies.
React components should do one thing internally and be used for one purpose externally. Pattern: Break large components into focused pieces. Violation: Component fetches data, filters it, handles pagination, and renders UI (four responsibilities, four actors). Fix: Custom hooks for logic separation: useFetchUsers() (API actor), useFilteredUsers() (business actor), usePagination() (UX actor), UserListDisplay (presentation actor). When useState and useEffect are connected, extract into custom hook. Example: const {users, loading} = useFetchUsers(); const filtered = useFilteredUsers(users, filter); return
Code analysis tools identify SRP violations through metrics, patterns, and coupling analysis. SonarQube (2025): Industry standard with 6,500+ rules across 35 languages, 7 million developers worldwide. Detects: (1) High complexity (cyclomatic complexity >15), (2) Low cohesion (LCOM metric), (3) High coupling (CBO metric), (4) Code smells ('Classes should not be coupled to too many other classes' rule targets SRP). Thresholds: Class >300 lines, >10 methods, >5 dependencies. Other tools: (2) ReSharper (C#/.NET) - suggests splitting classes, identifies coupling. (3) ESLint with complexity plugins (JavaScript/TypeScript). (4) CodeClimate - maintainability scores. (5) Visual Studio Code Analysis. Pattern detection: Classes named *Manager, *Utils, *Helper indicate unclear responsibility. TypeScript example: ESLint complexity rule flags class OrderService {validateOrder(), calculateTax(), processPayment(), sendEmail()} - four concerns detected. Limitations: Tools detect symptoms (size, complexity) not semantic violations (multiple actors). Require human judgment: Which actor owns this code? Best practice 2025: Combine SonarQube quality gates with actor-focused code reviews.
Don't apply SRP prematurely or rigidly - balance with YAGNI (You Aren't Gonna Need It) and pragmatism. YAGNI vs SRP tension: SRP adds extensibility, YAGNI avoids premature complexity. Rule of Three (2025 best practice): Tolerate coupling until pain becomes apparent (three variations), then refactor. When NOT to apply: (1) Trivial DTOs - interface UserDTO {id, name, email} doesn't need splitting. (2) Stable code - No changes in 2+ years, serves one actor clearly. (3) Prototypes - Requirements unclear, wait for patterns to emerge. (4) Responsibilities that always change together - If validation and business rules never change independently, combining them is pragmatic. (5) Over-fragmentation - 50 tiny classes harder to navigate than one cohesive class. Warning signs of over-engineering: Classes with single method, forced abstractions with no variation, complex navigation for simple tasks. 2025 guideline: Strict SOLID application without context leads to unnecessary complexity. Pragmatism over dogma: Apply SRP when benefits (easier testing, clearer ownership, reduced coupling) outweigh costs (more files, indirection). Context: SRP more valuable in large teams (clear actor ownership) than solo projects. Master knowing when to bend rules for the right reasons.
Dependency injection (DI) separates object creation responsibility from business logic - class focuses on its responsibility, DI container handles dependencies. Without DI: class UserService {private emailService = new EmailService(); private db = new Database();} - UserService responsible for creating dependencies (violates SRP). With DI: class UserService {constructor(private emailService: EmailService, private db: Database) {}} - dependencies injected, UserService only handles business logic. Benefits: (1) Single actor - UserService serves business requirements actor, not infrastructure actor. (2) Testability - inject mocks for testing. (3) Flexibility - swap implementations without changing UserService. DI containers (NestJS, InversifyJS, tsyringe) manage lifecycle and scope. Pattern: constructor injection for required dependencies, property injection for optional. TypeScript example: @injectable() class UserService {@inject(EmailService) emailService; processUser() {...}}. Warning: DI doesn't guarantee SRP - can still inject 10 dependencies (God class). Rule of thumb: >5 constructor parameters signals SRP violation. Refactor: split class into smaller services with fewer dependencies.
Each microservice should serve one business domain/actor (bounded context). Service boundaries aligned with organizational actors prevent coupling. Example: E-commerce - OrderService (sales actor), PaymentService (finance actor), InventoryService (warehouse actor), NotificationService (customer support actor). Each serves distinct stakeholder group with different change drivers. Violation: MonolithicService handles orders + payments + inventory - changes from finance actor affect warehouse operations. Granularity: Not too coarse (monolith), not too fine (nano-services). Sweet spot: Service per aggregate root (DDD) or business capability. Communication: Services communicate via events (loose coupling) or APIs. Testing: Each service testable in isolation, mock external dependencies. Deployment: Independent deployment per service (actor-driven changes isolated). Warning: Don't split by technical layer (APIService, DatabaseService) - split by business actor. 2025 trend: Team-per-service ownership reinforces actor alignment (team owns service serving their stakeholder). Conway's Law: System structure mirrors organization structure - microservices boundary IS actor boundary.
Database tables should represent single entities/actors, not mixed concerns. SRP in schema: Table serves one domain concept. Good: users (authentication actor), user_profiles (profile actor), user_preferences (UX actor) - three actors, three tables. Bad: users table with auth columns + profile columns + preferences columns + audit columns + billing columns - serves 5 actors, changes from billing team affect authentication. Normalization supports SRP: 3NF eliminates redundancy and mixed responsibilities. Example: orders table (sales actor) + order_items table (order details) + order_audit (compliance actor) - separate concerns. Violation: God table with 50+ columns serving multiple business units. Refactor: Vertical partitioning by actor - split into focused tables. Trade-offs: Over-normalization (100 tables for simple domain) vs under-normalization (one table for everything). Production: Use database views to aggregate data for specific actors without mixing storage responsibilities. Actor boundaries: Finance tables (invoices, payments), Operations tables (inventory, shipments), Customer tables (profiles, preferences). Warning: Don't confuse joins with SRP violations - joining user + profile for query is fine if stored separately.
Controllers should ONLY handle HTTP concerns - request/response parsing, validation, routing to services. Single responsibility: Translate HTTP to domain calls, domain results to HTTP. Pattern: @Controller() class UserController {@Post() createUser(@Body() dto: CreateUserDTO) {const user = await this.userService.create(dto); return {id: user.id, status: 201};}}. Controller handles: (1) HTTP routing (@Post, @Get), (2) DTO validation (@Body), (3) Response formatting (status codes, JSON). Does NOT handle: business logic, database access, external APIs. Those belong in services. Violation: Controller calculates tax, sends email, updates database directly - serves business actor, email actor, database actor. Fix: Move logic to UserService (business actor), EmailService (ops actor), UserRepository (data actor). Controller only coordinates. Actor: HTTP actor (API consumers who need REST interface). Changes from API versioning don't affect business logic. TypeScript example: Controller thin (10-30 lines per endpoint), services thick (business logic). Testing: Controller tests verify HTTP behavior (status codes, validation), service tests verify business logic. Modern frameworks (NestJS, Fastify) enforce this separation via decorators and DI.
Error handling should be separated by concern: error generation (business logic), error transformation (layer boundaries), error presentation (UI/API). Layered approach: (1) Domain layer throws domain exceptions - UserNotFoundException, InvalidEmailError. (2) Application layer catches and transforms - maps domain exceptions to application errors. (3) Infrastructure layer handles technical errors - DatabaseConnectionError, NetworkTimeout. (4) Presentation layer formats for display - HTTP status codes, user-friendly messages. Violation: Business logic catches database errors and returns HTTP responses - mixes three actors (business, infrastructure, API). TypeScript example: try {await userService.create()} catch (e) {if (e instanceof UserExistsError) return {status: 409}; throw e;}. Pattern: Use exception filters/middleware for cross-cutting error handling - @Catch() decorator in NestJS, Express error middleware. Single responsibility: ErrorLogger logs errors (ops actor), ErrorTransformer maps exceptions (API actor), ErrorNotifier sends alerts (on-call actor). Warning: Don't create God error handler that handles all errors everywhere - separate by actor. Production: Domain errors for business rules, technical errors for infrastructure, HTTP errors for API layer. Each layer responsible for its error concern.
Event handlers should handle one type of event for one actor. Pattern: UserCreatedHandler processes user creation (business actor), SendWelcomeEmailHandler sends email (ops actor), UpdateAnalyticsHandler tracks metrics (analytics actor) - three handlers for UserCreatedEvent, each serves one actor. Violation: Single handler does validation + email + analytics + audit logging - serves four actors. Event sourcing: Each event represents single fact (OrderPlaced, PaymentProcessed), aggregate applies events to update state. Responsibility: Event producer publishes facts, event consumers react independently. Actor alignment: Sales team owns OrderPlacedHandler (business logic), finance owns PaymentProcessedHandler (accounting), warehouse owns InventoryUpdatedHandler (operations). Benefits: Handlers independently deployable, one actor's changes don't affect others. TypeScript example: @EventHandler(UserCreatedEvent) class SendWelcomeEmailHandler {async handle(event) {await this.emailService.send(event.email);}}. Saga pattern: Orchestrator coordinates multi-step transactions but delegates work to specialized services (each SRP-compliant). Warning: Avoid God event handler that processes all events - separate by event type and actor.
Logging/monitoring should be separated from business logic via cross-cutting concerns (aspect-oriented programming, middleware). Pattern: Business classes focus on domain logic, logging handled by decorators/interceptors. Violation: class UserService {create(user) {this.logger.log('Creating user'); this.metrics.increment('user.created'); const result = this.db.save(user); this.logger.log('User created'); return result;}} - mixes business logic (database actor) with logging (ops actor) and metrics (monitoring actor). Fix: Use decorators - @Log() @Metrics('user.created') async create(user) {return this.db.save(user);}. Logging interceptor adds logs automatically. Separation: (1) Application logging (business events for audit) - separate logger. (2) Technical logging (errors, performance) - infrastructure concern. (3) Metrics (counters, timers) - monitoring service. (4) Tracing (distributed requests) - observability layer. TypeScript: NestJS interceptors, Winston logger, Prometheus client. Production: Structured logging with correlation IDs, log levels (DEBUG, INFO, ERROR), separate log storage per concern. Actor: Ops team owns logging infrastructure, developers own business event logging, SRE owns metrics. Warning: Over-logging violates SRP - log decisions should be configuration, not scattered in business code.
Interfaces should represent one role/contract for one actor. Good: interface Authenticator {login(), logout()} (auth actor), interface UserRepository {find(), save()} (database actor), interface EmailSender {send()} (notification actor) - focused contracts. Bad: interface IUser {login(), logout(), save(), find(), sendEmail(), generateReport()} - serves auth, database, email, reporting actors. Violation: Fat interface forces implementations to depend on methods they don't use (violates Interface Segregation Principle too). TypeScript example: interface PaymentProcessor {process(amount): Promise
Key metrics: (1) Coupling Between Objects (CBO) - counts dependencies. CBO >10 suggests God class. (2) Cyclomatic Complexity - methods >10 often mix concerns. (3) Lines of Code (LOC) - classes >300 lines likely violate SRP. (4) Number of methods - >15 public methods suggests multiple responsibilities. (5) Fan-out - class uses >7 other classes indicates mixed concerns. (6) LCOM (Lack of Cohesion of Methods) - measures how related methods are (note: removed from SonarQube 4.1+ due to false positives, available via third-party plugins like CKMetrics). Low LCOM (0-0.3) = high cohesion, high LCOM (>0.6) = low cohesion. SonarQube 2025 thresholds: Class LOC >300, method complexity >15, dependencies >10. Ideal targets: CBO <5, complexity <10, LOC <300. TypeScript tools: ts-morph, madge (dependency analysis), complexity-report, ESLint complexity plugins. Production: Track metrics in CI/CD, fail builds on violations. Example: Class with CBO=12, LOC=500, complexity >15 → refactor into 3-4 focused classes. Caveat: Metrics detect symptoms (size, coupling, complexity), not root cause - require human review to identify actual actors. Use metrics as signals, not absolute rules.
State management should separate concerns by actor: UI state (UX actor), server state (API actor), form state (validation actor), app state (business actor). Pattern: Redux/Zustand slices per domain. Good: userSlice (user actor), cartSlice (e-commerce actor), notificationSlice (ops actor) - separate reducers. Bad: Single God store with mixed concerns - app state + UI toggles + API cache + form errors. Violation: One reducer handles authentication + shopping cart + notifications - three actors. React: useState for component-local UI state, React Query for server state, Formik for form state, Context for global app state. Separation: (1) Server state (react-query, SWR) - caching, background updates. (2) Form state (Formik, react-hook-form) - validation, submission. (3) UI state (local useState) - modals, toggles. (4) Global state (Context, Redux) - auth, theme. TypeScript: Type state by domain - UserState, CartState, UIState. Actor alignment: Product team owns cart state, backend team owns API state, UX team owns UI state. Warning: Global state for everything violates SRP - use local state where possible. Production: Separate state updates by feature (feature flags), deploy state changes independently.
Repository separates data access (database actor) from business logic (domain actor). Pattern: interface UserRepository {find(), save(), delete()} - single responsibility is data persistence. Business logic stays in UserService. Violation: class UserService {saveUser() {const sql = 'INSERT INTO users...'; db.execute(sql); this.sendEmail();}} - mixes business logic, SQL, and email (three actors). Fix: UserRepository handles SQL (DBA actor), UserService handles business rules (business actor), EmailService handles notifications (ops actor). TypeScript example: class TypeORMUserRepository implements UserRepository {async find(id) {return this.repo.findOne(id);}}. Benefits: (1) Business logic doesn't know about SQL/NoSQL. (2) Easy to mock repository for testing. (3) Switch databases without changing business code. Actor: DBA owns repository schema changes, business owns service logic. Generic repository anti-pattern: class Repository
Service layer responsibility: Orchestrate business operations (business actor), coordinate domain objects and repositories, enforce transaction boundaries. Does: (1) Business workflows (multi-step operations). (2) Transaction management. (3) Call repositories for data. (4) Return DTOs to controllers. Does NOT: (1) HTTP parsing (controller responsibility). (2) SQL queries (repository responsibility). (3) Domain logic (domain model responsibility). (4) Presentation formatting (view responsibility). Example: class OrderService {async createOrder(dto) {const user = await this.userRepo.find(dto.userId); const order = Order.create(user, dto.items); await this.orderRepo.save(order); await this.emailService.send(order.confirmationEmail); return order;}}. Service orchestrates but delegates. Actor: Business stakeholders who define workflows. Changes to checkout process affect OrderService, not UserRepository or EmailController. Violation: Service contains SQL, HTTP responses, validation rules, formatting - four actors. Anti-pattern: Anemic service (only pass-through to repository) - lacks orchestration value. Transaction script pattern: Service per use case (CreateOrderService, CancelOrderService) vs one service per entity. Production: Service layer in application/use-case layer (Clean Architecture), domain logic in entities/value objects.
CQRS is SRP applied to read/write operations - separate models for commands (writes) and queries (reads). Pattern: CommandHandler modifies state (write actor), QueryHandler retrieves data (read actor) - different actors with different optimization needs. Example: CreateUserCommandHandler (business write operations), GetUserQueryHandler (read/reporting operations). Separation allows: (1) Write model optimized for transactions, validation, consistency. (2) Read model optimized for performance, denormalization, caching. (3) Different actors - CFO needs reports (queries), operations needs to create orders (commands). Violation: Single model handles create + update + complex reports - serves write actor and read actor with conflicting requirements (normalization vs denormalization). TypeScript: interface CommandHandler
Refactoring strategy: (1) Identify actors - list stakeholders who request changes (CFO, COO, CTO, users). (2) Map methods to actors - group methods by which actor they serve. (3) Extract classes per actor - UserAuthenticator (IT actor), UserProfileManager (users actor), UserReportGenerator (CFO actor). (4) Update dependencies - inject new classes where needed. (5) Migrate incrementally - keep old class as facade initially, gradually migrate callers. TypeScript example: class User {login(), updateProfile(), generateReport()} → Extract: class Authenticator {login()}, class ProfileManager {update()}, class ReportGenerator {generate()}. Techniques: Extract Class, Extract Interface, Move Method. Tools: IDE refactoring (VS Code, WebStorm) automates extraction. Testing: Write tests before refactoring (characterization tests), ensure tests pass after each step. Strangler Fig pattern: Create new classes alongside old, migrate usage gradually, deprecate old class. Warning: Don't extract blindly - verify actors are truly distinct (different change drivers). Production: Refactor when adding features (Boy Scout Rule), not big-bang rewrites. Allocate 20% of sprint to refactoring. Metrics: Track class size and complexity over time, celebrate reductions.
SRP can hurt performance when excessive abstraction adds overhead, but usually negligible. Trade-offs: (1) Indirection cost - calling 5 small classes vs 1 method in God class adds function call overhead (nanoseconds in TypeScript/JavaScript). (2) Memory - more objects = higher memory usage. (3) Instantiation - creating multiple service instances slower than one. Real impact: Usually <1% performance difference in application code (I/O dominates). Example: Splitting calculateTax() into TaxRateProvider, TaxCalculator, TaxFormatter adds 3 method calls (0.0001ms) - irrelevant compared to database query (10ms). When it matters: (1) Hot paths - inner loops processing millions of items (game engines, image processing). (2) Embedded systems - memory constrained. (3) Real-time systems - microsecond latency requirements. Solution: Profile first, optimize hot paths only. Extract performance-critical code into optimized implementation while keeping SRP elsewhere. TypeScript: V8 JIT optimizes small functions - modern JS engines inline calls, eliminating overhead. Production: 99% of code is not performance-critical - favor SRP for maintainability. For 1% hot paths: optimize after measuring. Warning: Premature optimization citing performance to avoid SRP is anti-pattern - measure first.
DDD entities and value objects follow SRP - each represents one domain concept for one actor. Entity responsibility: Maintain identity and invariants for its aggregate. Example: Order entity enforces order rules (sales actor), Payment entity enforces payment rules (finance actor), Shipment entity enforces shipping rules (operations actor). Violation: Order entity handles payment processing + inventory updates + shipping - serves three actors. DDD layering: (1) Domain layer - pure business logic (business actor). (2) Application layer - use cases, orchestration (workflow actor). (3) Infrastructure layer - database, external APIs (technical actor). Aggregate root: Single entry point to aggregate, enforces business rules for its bounded context. Actor alignment: Bounded contexts map to actors - Sales context (sales team), Billing context (finance team), Inventory context (warehouse team). Value objects: Immutable, represent concepts (Email, Money, Address) - single responsibility is value equality and validation. TypeScript: class Order {addItem(), applyDiscount()} - business rules only, no database or HTTP code. Repository per aggregate root. Production: Ubiquitous language per context, each context serves distinct stakeholder group. DDD is SRP at architecture level.
A module should be responsible to one, and only one, actor. Robert Martin's original definition: 'A class should have only one reason to change.' Clean Architecture (2017) refined this: 'A module should be responsible to one, and only one, actor' - where 'actor' represents a group of stakeholders who would request changes for the same reason. Example: UserController (HTTP actor), UserService (business rules actor), UserRepository (database actor) - three distinct actors. Violation: UserManager serves CFO (reporting), COO (business rules), CTO (infrastructure) - changes for one actor break another's needs. Benefits: Changes isolated to one stakeholder group, easier testing, clearer ownership. Historical note: 'Reason to change' caused confusion - actor-based definition is more precise and prevents unexpected side effects when one group's requirements change.
Ask: 'Does this class serve more than one actor?' or 'Does it have multiple reasons to change?' Red flags: (1) Class name with 'Manager', 'Utils', 'Helper', 'And', 'Or' - indicates unclear responsibility. (2) God class (>300 lines, 15+ methods). (3) Methods dealing with unrelated concerns (sendEmail + calculateTax). (4) Multiple import groups (HTTP + database + email libs). (5) Testing requires mocking 5+ dependencies. (6) Hard to name in one sentence without 'and'. Example: class Invoice {calculate(), formatForPrint(), saveToDB()} serves three actors: accounting (calculation), printing team (format), DBAs (schema). Refactor: InvoiceCalculator, InvoicePrinter, InvoiceRepository. Detection tools: SonarQube, ReSharper, Visual Studio Code Analysis flag high complexity and low cohesion. Warning sign: Everyone adds features to this class because it's 'easy to access' - leads to monster God class over time.
SRP is class-level (each class one actor), Separation of Concerns (SoC) is system-level (architecture organized by concern). Robert Martin: 'Reasons for change are people' - SRP is about organizational actors who request changes. SRP is Martin's practical refinement of the broader SoC principle, specifically focused on actors and their reasons for requesting changes. SoC (Dijkstra): Broad principle dividing system into distinct concerns (technical, architectural, or organizational). Example: Frontend ↔ Backend API ↔ Database (SoC at system level). SRP within SoC: Backend has UserController (HTTP actor), UserService (business actor), UserRepository (data actor) - each following SRP. Violation: Backend separates layers but UserService handles validation + business + caching (multiple actors). SRP asks 'which actor owns this change?' (people-based), SoC asks 'what is the concern?' (feature-based). Clean Architecture applies both: SoC for layers (presentation, business, data), SRP within each layer. TypeScript example: class EmployeeRepository (DBA actor) vs class EmployeeReportFormatter (CFO actor) vs class EmployeeSalaryCalculator (accounting actor) - three actors, three classes.
Functions should do one thing at one abstraction level. SRP for functions: Each performs one cohesive task. Good: function validateEmail(email) {return /regex/.test(email);} - one task, clear name. Bad: function processUser(user) {validateEmail(user.email); hashPassword(user.password); saveToDatabase(user); sendWelcomeEmail(user);} - four responsibilities at different abstraction levels. Refactor: Create four single-responsibility functions, processUser() orchestrates. Violations: (1) Name with 'and' (validateAndSave), (2) Multiple return types (data | error | null), (3) Vague name (handleUserStuff). Robert Martin: 'Functions should be small and do one thing.' Test: Describe in one sentence without 'and'? If no, split it. SRP applies recursively: system → modules → classes → methods → functions. Each level has single responsibility at its scope. Modern: React functional components follow this - small focused components, custom hooks extract reusable logic.
Violation 1: class User {login(), logout(), updateProfile(), sendEmail(), generateReport()} - mixes auth (IT actor), profiles (users actor), email (ops actor), reporting (business actor). Fix: AuthService, UserProfileService, EmailService, ReportGenerator - four actors, four classes. Violation 2: class OrderController {validateOrder(), calculateTax(), processPayment(), updateInventory(), sendConfirmation()} - handles HTTP, validation, tax rules, payment, inventory, notifications. Fix: OrderController (HTTP only), OrderValidator, TaxCalculator, PaymentProcessor, InventoryManager, NotificationService. Violation 3 (E-commerce): One class fetches data from API, processes it, saves to database, sends email. Fix: OrderAPIClient, OrderProcessor, OrderRepository, OrderNotifier. Mobile app: Split data fetching (network actor) from UI processing (UX actor) using UserDataProcessor and UIController. React: Component handles fetch + filter + display. Fix: useFetchData() hook, useFilteredData() hook, DisplayComponent. Result: Each actor's changes isolated.
SRP is crucial for testability - enables unit testing in isolation with mocked dependencies. Connection: To test class, identify and inject dependencies. To identify dependency = identify responsibility. Single responsibility = minimal inputs/outputs/decision points = easy tests. Pattern: class OrderService {constructor(private paymentProcessor: PaymentProcessor, private inventory: InventoryManager)} - two clear dependencies, easy to mock. Testing SRP-compliant code: Mock dependencies, verify they're called correctly, test single concern. Example: Test OrderService processes payment via PaymentProcessor without needing real payment gateway or database. Violation consequences: class OrderService handles payment + inventory + email → testing requires mocking 10+ dependencies, brittle tests, hard to isolate failures. TDD benefits: Test-Driven Development forces identifying responsibilities upfront - if class hard to test, likely violates SRP. Dependency injection enables both SRP and testability: inject PaymentProcessor interface, vary behavior without changing OrderService. Tools: Use DI frameworks (NestJS, InversifyJS) to manage dependencies.
React components should do one thing internally and be used for one purpose externally. Pattern: Break large components into focused pieces. Violation: Component fetches data, filters it, handles pagination, and renders UI (four responsibilities, four actors). Fix: Custom hooks for logic separation: useFetchUsers() (API actor), useFilteredUsers() (business actor), usePagination() (UX actor), UserListDisplay (presentation actor). When useState and useEffect are connected, extract into custom hook. Example: const {users, loading} = useFetchUsers(); const filtered = useFilteredUsers(users, filter); return
Code analysis tools identify SRP violations through metrics, patterns, and coupling analysis. SonarQube (2025): Industry standard with 6,500+ rules across 35 languages, 7 million developers worldwide. Detects: (1) High complexity (cyclomatic complexity >15), (2) Low cohesion (LCOM metric), (3) High coupling (CBO metric), (4) Code smells ('Classes should not be coupled to too many other classes' rule targets SRP). Thresholds: Class >300 lines, >10 methods, >5 dependencies. Other tools: (2) ReSharper (C#/.NET) - suggests splitting classes, identifies coupling. (3) ESLint with complexity plugins (JavaScript/TypeScript). (4) CodeClimate - maintainability scores. (5) Visual Studio Code Analysis. Pattern detection: Classes named *Manager, *Utils, *Helper indicate unclear responsibility. TypeScript example: ESLint complexity rule flags class OrderService {validateOrder(), calculateTax(), processPayment(), sendEmail()} - four concerns detected. Limitations: Tools detect symptoms (size, complexity) not semantic violations (multiple actors). Require human judgment: Which actor owns this code? Best practice 2025: Combine SonarQube quality gates with actor-focused code reviews.
Don't apply SRP prematurely or rigidly - balance with YAGNI (You Aren't Gonna Need It) and pragmatism. YAGNI vs SRP tension: SRP adds extensibility, YAGNI avoids premature complexity. Rule of Three (2025 best practice): Tolerate coupling until pain becomes apparent (three variations), then refactor. When NOT to apply: (1) Trivial DTOs - interface UserDTO {id, name, email} doesn't need splitting. (2) Stable code - No changes in 2+ years, serves one actor clearly. (3) Prototypes - Requirements unclear, wait for patterns to emerge. (4) Responsibilities that always change together - If validation and business rules never change independently, combining them is pragmatic. (5) Over-fragmentation - 50 tiny classes harder to navigate than one cohesive class. Warning signs of over-engineering: Classes with single method, forced abstractions with no variation, complex navigation for simple tasks. 2025 guideline: Strict SOLID application without context leads to unnecessary complexity. Pragmatism over dogma: Apply SRP when benefits (easier testing, clearer ownership, reduced coupling) outweigh costs (more files, indirection). Context: SRP more valuable in large teams (clear actor ownership) than solo projects. Master knowing when to bend rules for the right reasons.
Dependency injection (DI) separates object creation responsibility from business logic - class focuses on its responsibility, DI container handles dependencies. Without DI: class UserService {private emailService = new EmailService(); private db = new Database();} - UserService responsible for creating dependencies (violates SRP). With DI: class UserService {constructor(private emailService: EmailService, private db: Database) {}} - dependencies injected, UserService only handles business logic. Benefits: (1) Single actor - UserService serves business requirements actor, not infrastructure actor. (2) Testability - inject mocks for testing. (3) Flexibility - swap implementations without changing UserService. DI containers (NestJS, InversifyJS, tsyringe) manage lifecycle and scope. Pattern: constructor injection for required dependencies, property injection for optional. TypeScript example: @injectable() class UserService {@inject(EmailService) emailService; processUser() {...}}. Warning: DI doesn't guarantee SRP - can still inject 10 dependencies (God class). Rule of thumb: >5 constructor parameters signals SRP violation. Refactor: split class into smaller services with fewer dependencies.
Each microservice should serve one business domain/actor (bounded context). Service boundaries aligned with organizational actors prevent coupling. Example: E-commerce - OrderService (sales actor), PaymentService (finance actor), InventoryService (warehouse actor), NotificationService (customer support actor). Each serves distinct stakeholder group with different change drivers. Violation: MonolithicService handles orders + payments + inventory - changes from finance actor affect warehouse operations. Granularity: Not too coarse (monolith), not too fine (nano-services). Sweet spot: Service per aggregate root (DDD) or business capability. Communication: Services communicate via events (loose coupling) or APIs. Testing: Each service testable in isolation, mock external dependencies. Deployment: Independent deployment per service (actor-driven changes isolated). Warning: Don't split by technical layer (APIService, DatabaseService) - split by business actor. 2025 trend: Team-per-service ownership reinforces actor alignment (team owns service serving their stakeholder). Conway's Law: System structure mirrors organization structure - microservices boundary IS actor boundary.
Database tables should represent single entities/actors, not mixed concerns. SRP in schema: Table serves one domain concept. Good: users (authentication actor), user_profiles (profile actor), user_preferences (UX actor) - three actors, three tables. Bad: users table with auth columns + profile columns + preferences columns + audit columns + billing columns - serves 5 actors, changes from billing team affect authentication. Normalization supports SRP: 3NF eliminates redundancy and mixed responsibilities. Example: orders table (sales actor) + order_items table (order details) + order_audit (compliance actor) - separate concerns. Violation: God table with 50+ columns serving multiple business units. Refactor: Vertical partitioning by actor - split into focused tables. Trade-offs: Over-normalization (100 tables for simple domain) vs under-normalization (one table for everything). Production: Use database views to aggregate data for specific actors without mixing storage responsibilities. Actor boundaries: Finance tables (invoices, payments), Operations tables (inventory, shipments), Customer tables (profiles, preferences). Warning: Don't confuse joins with SRP violations - joining user + profile for query is fine if stored separately.
Controllers should ONLY handle HTTP concerns - request/response parsing, validation, routing to services. Single responsibility: Translate HTTP to domain calls, domain results to HTTP. Pattern: @Controller() class UserController {@Post() createUser(@Body() dto: CreateUserDTO) {const user = await this.userService.create(dto); return {id: user.id, status: 201};}}. Controller handles: (1) HTTP routing (@Post, @Get), (2) DTO validation (@Body), (3) Response formatting (status codes, JSON). Does NOT handle: business logic, database access, external APIs. Those belong in services. Violation: Controller calculates tax, sends email, updates database directly - serves business actor, email actor, database actor. Fix: Move logic to UserService (business actor), EmailService (ops actor), UserRepository (data actor). Controller only coordinates. Actor: HTTP actor (API consumers who need REST interface). Changes from API versioning don't affect business logic. TypeScript example: Controller thin (10-30 lines per endpoint), services thick (business logic). Testing: Controller tests verify HTTP behavior (status codes, validation), service tests verify business logic. Modern frameworks (NestJS, Fastify) enforce this separation via decorators and DI.
Error handling should be separated by concern: error generation (business logic), error transformation (layer boundaries), error presentation (UI/API). Layered approach: (1) Domain layer throws domain exceptions - UserNotFoundException, InvalidEmailError. (2) Application layer catches and transforms - maps domain exceptions to application errors. (3) Infrastructure layer handles technical errors - DatabaseConnectionError, NetworkTimeout. (4) Presentation layer formats for display - HTTP status codes, user-friendly messages. Violation: Business logic catches database errors and returns HTTP responses - mixes three actors (business, infrastructure, API). TypeScript example: try {await userService.create()} catch (e) {if (e instanceof UserExistsError) return {status: 409}; throw e;}. Pattern: Use exception filters/middleware for cross-cutting error handling - @Catch() decorator in NestJS, Express error middleware. Single responsibility: ErrorLogger logs errors (ops actor), ErrorTransformer maps exceptions (API actor), ErrorNotifier sends alerts (on-call actor). Warning: Don't create God error handler that handles all errors everywhere - separate by actor. Production: Domain errors for business rules, technical errors for infrastructure, HTTP errors for API layer. Each layer responsible for its error concern.
Event handlers should handle one type of event for one actor. Pattern: UserCreatedHandler processes user creation (business actor), SendWelcomeEmailHandler sends email (ops actor), UpdateAnalyticsHandler tracks metrics (analytics actor) - three handlers for UserCreatedEvent, each serves one actor. Violation: Single handler does validation + email + analytics + audit logging - serves four actors. Event sourcing: Each event represents single fact (OrderPlaced, PaymentProcessed), aggregate applies events to update state. Responsibility: Event producer publishes facts, event consumers react independently. Actor alignment: Sales team owns OrderPlacedHandler (business logic), finance owns PaymentProcessedHandler (accounting), warehouse owns InventoryUpdatedHandler (operations). Benefits: Handlers independently deployable, one actor's changes don't affect others. TypeScript example: @EventHandler(UserCreatedEvent) class SendWelcomeEmailHandler {async handle(event) {await this.emailService.send(event.email);}}. Saga pattern: Orchestrator coordinates multi-step transactions but delegates work to specialized services (each SRP-compliant). Warning: Avoid God event handler that processes all events - separate by event type and actor.
Logging/monitoring should be separated from business logic via cross-cutting concerns (aspect-oriented programming, middleware). Pattern: Business classes focus on domain logic, logging handled by decorators/interceptors. Violation: class UserService {create(user) {this.logger.log('Creating user'); this.metrics.increment('user.created'); const result = this.db.save(user); this.logger.log('User created'); return result;}} - mixes business logic (database actor) with logging (ops actor) and metrics (monitoring actor). Fix: Use decorators - @Log() @Metrics('user.created') async create(user) {return this.db.save(user);}. Logging interceptor adds logs automatically. Separation: (1) Application logging (business events for audit) - separate logger. (2) Technical logging (errors, performance) - infrastructure concern. (3) Metrics (counters, timers) - monitoring service. (4) Tracing (distributed requests) - observability layer. TypeScript: NestJS interceptors, Winston logger, Prometheus client. Production: Structured logging with correlation IDs, log levels (DEBUG, INFO, ERROR), separate log storage per concern. Actor: Ops team owns logging infrastructure, developers own business event logging, SRE owns metrics. Warning: Over-logging violates SRP - log decisions should be configuration, not scattered in business code.
Interfaces should represent one role/contract for one actor. Good: interface Authenticator {login(), logout()} (auth actor), interface UserRepository {find(), save()} (database actor), interface EmailSender {send()} (notification actor) - focused contracts. Bad: interface IUser {login(), logout(), save(), find(), sendEmail(), generateReport()} - serves auth, database, email, reporting actors. Violation: Fat interface forces implementations to depend on methods they don't use (violates Interface Segregation Principle too). TypeScript example: interface PaymentProcessor {process(amount): Promise
Key metrics: (1) Coupling Between Objects (CBO) - counts dependencies. CBO >10 suggests God class. (2) Cyclomatic Complexity - methods >10 often mix concerns. (3) Lines of Code (LOC) - classes >300 lines likely violate SRP. (4) Number of methods - >15 public methods suggests multiple responsibilities. (5) Fan-out - class uses >7 other classes indicates mixed concerns. (6) LCOM (Lack of Cohesion of Methods) - measures how related methods are (note: removed from SonarQube 4.1+ due to false positives, available via third-party plugins like CKMetrics). Low LCOM (0-0.3) = high cohesion, high LCOM (>0.6) = low cohesion. SonarQube 2025 thresholds: Class LOC >300, method complexity >15, dependencies >10. Ideal targets: CBO <5, complexity <10, LOC <300. TypeScript tools: ts-morph, madge (dependency analysis), complexity-report, ESLint complexity plugins. Production: Track metrics in CI/CD, fail builds on violations. Example: Class with CBO=12, LOC=500, complexity >15 → refactor into 3-4 focused classes. Caveat: Metrics detect symptoms (size, coupling, complexity), not root cause - require human review to identify actual actors. Use metrics as signals, not absolute rules.
State management should separate concerns by actor: UI state (UX actor), server state (API actor), form state (validation actor), app state (business actor). Pattern: Redux/Zustand slices per domain. Good: userSlice (user actor), cartSlice (e-commerce actor), notificationSlice (ops actor) - separate reducers. Bad: Single God store with mixed concerns - app state + UI toggles + API cache + form errors. Violation: One reducer handles authentication + shopping cart + notifications - three actors. React: useState for component-local UI state, React Query for server state, Formik for form state, Context for global app state. Separation: (1) Server state (react-query, SWR) - caching, background updates. (2) Form state (Formik, react-hook-form) - validation, submission. (3) UI state (local useState) - modals, toggles. (4) Global state (Context, Redux) - auth, theme. TypeScript: Type state by domain - UserState, CartState, UIState. Actor alignment: Product team owns cart state, backend team owns API state, UX team owns UI state. Warning: Global state for everything violates SRP - use local state where possible. Production: Separate state updates by feature (feature flags), deploy state changes independently.
Repository separates data access (database actor) from business logic (domain actor). Pattern: interface UserRepository {find(), save(), delete()} - single responsibility is data persistence. Business logic stays in UserService. Violation: class UserService {saveUser() {const sql = 'INSERT INTO users...'; db.execute(sql); this.sendEmail();}} - mixes business logic, SQL, and email (three actors). Fix: UserRepository handles SQL (DBA actor), UserService handles business rules (business actor), EmailService handles notifications (ops actor). TypeScript example: class TypeORMUserRepository implements UserRepository {async find(id) {return this.repo.findOne(id);}}. Benefits: (1) Business logic doesn't know about SQL/NoSQL. (2) Easy to mock repository for testing. (3) Switch databases without changing business code. Actor: DBA owns repository schema changes, business owns service logic. Generic repository anti-pattern: class Repository
Service layer responsibility: Orchestrate business operations (business actor), coordinate domain objects and repositories, enforce transaction boundaries. Does: (1) Business workflows (multi-step operations). (2) Transaction management. (3) Call repositories for data. (4) Return DTOs to controllers. Does NOT: (1) HTTP parsing (controller responsibility). (2) SQL queries (repository responsibility). (3) Domain logic (domain model responsibility). (4) Presentation formatting (view responsibility). Example: class OrderService {async createOrder(dto) {const user = await this.userRepo.find(dto.userId); const order = Order.create(user, dto.items); await this.orderRepo.save(order); await this.emailService.send(order.confirmationEmail); return order;}}. Service orchestrates but delegates. Actor: Business stakeholders who define workflows. Changes to checkout process affect OrderService, not UserRepository or EmailController. Violation: Service contains SQL, HTTP responses, validation rules, formatting - four actors. Anti-pattern: Anemic service (only pass-through to repository) - lacks orchestration value. Transaction script pattern: Service per use case (CreateOrderService, CancelOrderService) vs one service per entity. Production: Service layer in application/use-case layer (Clean Architecture), domain logic in entities/value objects.
CQRS is SRP applied to read/write operations - separate models for commands (writes) and queries (reads). Pattern: CommandHandler modifies state (write actor), QueryHandler retrieves data (read actor) - different actors with different optimization needs. Example: CreateUserCommandHandler (business write operations), GetUserQueryHandler (read/reporting operations). Separation allows: (1) Write model optimized for transactions, validation, consistency. (2) Read model optimized for performance, denormalization, caching. (3) Different actors - CFO needs reports (queries), operations needs to create orders (commands). Violation: Single model handles create + update + complex reports - serves write actor and read actor with conflicting requirements (normalization vs denormalization). TypeScript: interface CommandHandler
Refactoring strategy: (1) Identify actors - list stakeholders who request changes (CFO, COO, CTO, users). (2) Map methods to actors - group methods by which actor they serve. (3) Extract classes per actor - UserAuthenticator (IT actor), UserProfileManager (users actor), UserReportGenerator (CFO actor). (4) Update dependencies - inject new classes where needed. (5) Migrate incrementally - keep old class as facade initially, gradually migrate callers. TypeScript example: class User {login(), updateProfile(), generateReport()} → Extract: class Authenticator {login()}, class ProfileManager {update()}, class ReportGenerator {generate()}. Techniques: Extract Class, Extract Interface, Move Method. Tools: IDE refactoring (VS Code, WebStorm) automates extraction. Testing: Write tests before refactoring (characterization tests), ensure tests pass after each step. Strangler Fig pattern: Create new classes alongside old, migrate usage gradually, deprecate old class. Warning: Don't extract blindly - verify actors are truly distinct (different change drivers). Production: Refactor when adding features (Boy Scout Rule), not big-bang rewrites. Allocate 20% of sprint to refactoring. Metrics: Track class size and complexity over time, celebrate reductions.
SRP can hurt performance when excessive abstraction adds overhead, but usually negligible. Trade-offs: (1) Indirection cost - calling 5 small classes vs 1 method in God class adds function call overhead (nanoseconds in TypeScript/JavaScript). (2) Memory - more objects = higher memory usage. (3) Instantiation - creating multiple service instances slower than one. Real impact: Usually <1% performance difference in application code (I/O dominates). Example: Splitting calculateTax() into TaxRateProvider, TaxCalculator, TaxFormatter adds 3 method calls (0.0001ms) - irrelevant compared to database query (10ms). When it matters: (1) Hot paths - inner loops processing millions of items (game engines, image processing). (2) Embedded systems - memory constrained. (3) Real-time systems - microsecond latency requirements. Solution: Profile first, optimize hot paths only. Extract performance-critical code into optimized implementation while keeping SRP elsewhere. TypeScript: V8 JIT optimizes small functions - modern JS engines inline calls, eliminating overhead. Production: 99% of code is not performance-critical - favor SRP for maintainability. For 1% hot paths: optimize after measuring. Warning: Premature optimization citing performance to avoid SRP is anti-pattern - measure first.
DDD entities and value objects follow SRP - each represents one domain concept for one actor. Entity responsibility: Maintain identity and invariants for its aggregate. Example: Order entity enforces order rules (sales actor), Payment entity enforces payment rules (finance actor), Shipment entity enforces shipping rules (operations actor). Violation: Order entity handles payment processing + inventory updates + shipping - serves three actors. DDD layering: (1) Domain layer - pure business logic (business actor). (2) Application layer - use cases, orchestration (workflow actor). (3) Infrastructure layer - database, external APIs (technical actor). Aggregate root: Single entry point to aggregate, enforces business rules for its bounded context. Actor alignment: Bounded contexts map to actors - Sales context (sales team), Billing context (finance team), Inventory context (warehouse team). Value objects: Immutable, represent concepts (Email, Money, Address) - single responsibility is value equality and validation. TypeScript: class Order {addItem(), applyDiscount()} - business rules only, no database or HTTP code. Repository per aggregate root. Production: Ubiquitous language per context, each context serves distinct stakeholder group. DDD is SRP at architecture level.