# Single Responsibility Principle (SRP) > "A class should have only one reason to change." — Robert C. Martin ## Problem A class accumulates multiple responsibilities over time. It handles business logic **and** persistence **and** formatting **and** notification. When any one of those concerns changes, you must modify the class — risking breakage in unrelated behavior. Testing becomes difficult because you cannot exercise one concern without dragging in the others. **Symptoms of SRP violation:** - A class has methods that serve different actors (e.g., `calculate_pay()` for accounting, `generate_report()` for management) - Changes to logging break business logic - You cannot unit-test validation without setting up a database connection - The class file grows endlessly with unrelated methods ## Solution Identify the distinct reasons the class might change. Extract each responsibility into its own class or module. Each resulting unit has a single, well-defined purpose and changes only when that purpose evolves. ## When to Use - A class has grown to serve multiple stakeholders or concerns - You find yourself modifying one method and worrying about breaking another - Unit testing requires elaborate setup because concerns are intertwined - Multiple developers frequently have merge conflicts in the same file ## When to Avoid - The class is genuinely small and cohesive (splitting would just scatter trivial logic) - You are writing a short script or throwaway prototype - The "responsibilities" are tightly coupled by nature and splitting them would introduce unnecessary indirection ## Pseudocode ### Before — mixed responsibilities ``` CLASS User: name, email METHOD validate(): IF name IS EMPTY OR email DOES NOT CONTAIN "@": RAISE ValidationError METHOD save_to_database(): DB.execute("INSERT INTO users ...") METHOD send_welcome_email(): EmailService.send(to=email, subject="Welcome!") ``` ### After — each class owns one concern ``` CLASS User: name, email METHOD validate(): IF name IS EMPTY OR email DOES NOT CONTAIN "@": RAISE ValidationError CLASS UserRepository: METHOD save(user): DB.execute("INSERT INTO users ...") CLASS UserNotifier: METHOD send_welcome(user): EmailService.send(to=user.email, subject="Welcome!") ``` Now `User` changes only if validation rules change, `UserRepository` changes only if the storage mechanism changes, and `UserNotifier` changes only if notification requirements change. ## Python ```python """ Single Responsibility Principle — Python BEFORE: Report class generates content AND formats AND emails. AFTER: Three focused classes, each with one job. """ # === BEFORE: Violating SRP === class ReportBefore: def __init__(self, title: str, data: list[dict]): self.title = title self.data = data def generate_content(self) -> str: lines = [self.title, "=" * len(self.title)] for row in self.data: lines.append(f" {row['item']}: ${row['amount']:.2f}") total = sum(r["amount"] for r in self.data) lines.append(f" TOTAL: ${total:.2f}") return "\n".join(lines) def print_report(self) -> None: # Formatting concern mixed in content = self.generate_content() border = "#" * 40 print(f"{border}\n{content}\n{border}") def email_report(self, recipient: str) -> None: # Notification concern mixed in content = self.generate_content() print(f"[EMAIL] Sending to {recipient}:\n{content}") # === AFTER: Following SRP === class Report: """Responsible only for holding and generating report content.""" def __init__(self, title: str, data: list[dict]): self.title = title self.data = data def generate_content(self) -> str: lines = [self.title, "=" * len(self.title)] for row in self.data: lines.append(f" {row['item']}: ${row['amount']:.2f}") total = sum(r["amount"] for r in self.data) lines.append(f" TOTAL: ${total:.2f}") return "\n".join(lines) class ReportPrinter: """Responsible only for formatting and printing a report.""" def print_report(self, report: Report) -> None: content = report.generate_content() border = "#" * 40 print(f"{border}\n{content}\n{border}") class ReportEmailer: """Responsible only for emailing a report.""" def email_report(self, report: Report, recipient: str) -> None: content = report.generate_content() print(f"[EMAIL] Sending to {recipient}:\n{content}") # === Demo === def main(): data = [ {"item": "Widget A", "amount": 29.99}, {"item": "Widget B", "amount": 14.50}, {"item": "Widget C", "amount": 45.00}, ] print("--- BEFORE (SRP violated) ---") old_report = ReportBefore("Q1 Sales", data) old_report.print_report() old_report.email_report("boss@example.com") print("\n--- AFTER (SRP applied) ---") report = Report("Q1 Sales", data) printer = ReportPrinter() emailer = ReportEmailer() printer.print_report(report) emailer.email_report(report, "boss@example.com") if __name__ == "__main__": main() ``` ## Go ```go // Single Responsibility Principle — Go // // BEFORE: A single UserService struct handles validation, persistence, and notification. // AFTER: Three separate structs, each owning one concern. package main import ( "errors" "fmt" "strings" ) // ============================================================ // BEFORE: Violating SRP — one struct does everything // ============================================================ type UserServiceBefore struct{} func (s *UserServiceBefore) Validate(name, email string) error { if name == "" { return errors.New("name is required") } if !strings.Contains(email, "@") { return errors.New("invalid email") } return nil } func (s *UserServiceBefore) Save(name, email string) { fmt.Printf(" [DB] INSERT INTO users (name, email) VALUES ('%s', '%s')\n", name, email) } func (s *UserServiceBefore) SendWelcome(email string) { fmt.Printf(" [EMAIL] Welcome message sent to %s\n", email) } // ============================================================ // AFTER: Following SRP — separate structs per concern // ============================================================ // User holds only the data. type User struct { Name string Email string } // UserValidator is responsible only for validation. type UserValidator struct{} func (v *UserValidator) Validate(u User) error { if u.Name == "" { return errors.New("name is required") } if !strings.Contains(u.Email, "@") { return errors.New("invalid email") } fmt.Printf(" [VALID] User '%s' passed validation\n", u.Name) return nil } // UserRepository is responsible only for persistence. type UserRepository struct{} func (r *UserRepository) Save(u User) { fmt.Printf(" [DB] INSERT INTO users (name, email) VALUES ('%s', '%s')\n", u.Name, u.Email) } // UserNotifier is responsible only for notifications. type UserNotifier struct{} func (n *UserNotifier) SendWelcome(u User) { fmt.Printf(" [EMAIL] Welcome message sent to %s\n", u.Email) } // ============================================================ // Demo // ============================================================ func main() { fmt.Println("--- BEFORE (SRP violated) ---") svc := &UserServiceBefore{} if err := svc.Validate("Alice", "alice@example.com"); err != nil { fmt.Println(" Error:", err) } else { svc.Save("Alice", "alice@example.com") svc.SendWelcome("alice@example.com") } fmt.Println("\n--- AFTER (SRP applied) ---") user := User{Name: "Alice", Email: "alice@example.com"} validator := &UserValidator{} repo := &UserRepository{} notifier := &UserNotifier{} if err := validator.Validate(user); err != nil { fmt.Println(" Error:", err) return } repo.Save(user) notifier.SendWelcome(user) } ``` ## JavaScript ```javascript // Single Responsibility Principle — JavaScript // // BEFORE: OrderProcessor handles price calculation, persistence, AND receipt formatting. // AFTER: Split into OrderCalculator, OrderRepository, and ReceiptFormatter. // ============================================================ // BEFORE: Violating SRP // ============================================================ class OrderProcessorBefore { constructor(items) { this.items = items; } calculateTotal() { return this.items.reduce((sum, item) => sum + item.price * item.qty, 0); } saveToDatabase() { const total = this.calculateTotal(); console.log(` [DB] Saved order — ${this.items.length} items, total: $${total.toFixed(2)}`); } printReceipt() { console.log(" --- Receipt ---"); for (const item of this.items) { console.log(` ${item.name} x${item.qty} = $${(item.price * item.qty).toFixed(2)}`); } console.log(` TOTAL: $${this.calculateTotal().toFixed(2)}`); console.log(" ----------------"); } } // ============================================================ // AFTER: Following SRP // ============================================================ class Order { constructor(items) { this.items = items; } } class OrderCalculator { /** Responsible only for price computation. */ calculateTotal(order) { return order.items.reduce((sum, item) => sum + item.price * item.qty, 0); } } class OrderRepository { /** Responsible only for persistence. */ save(order, total) { console.log(` [DB] Saved order — ${order.items.length} items, total: $${total.toFixed(2)}`); } } class ReceiptFormatter { /** Responsible only for formatting and printing receipts. */ print(order, total) { console.log(" --- Receipt ---"); for (const item of order.items) { console.log(` ${item.name} x${item.qty} = $${(item.price * item.qty).toFixed(2)}`); } console.log(` TOTAL: $${total.toFixed(2)}`); console.log(" ----------------"); } } // ============================================================ // Demo // ============================================================ function main() { const items = [ { name: "Keyboard", price: 49.99, qty: 1 }, { name: "Mouse", price: 29.99, qty: 2 }, { name: "USB Cable", price: 9.99, qty: 3 }, ]; console.log("--- BEFORE (SRP violated) ---"); const oldProcessor = new OrderProcessorBefore(items); oldProcessor.saveToDatabase(); oldProcessor.printReceipt(); console.log("\n--- AFTER (SRP applied) ---"); const order = new Order(items); const calculator = new OrderCalculator(); const repository = new OrderRepository(); const formatter = new ReceiptFormatter(); const total = calculator.calculateTotal(order); repository.save(order, total); formatter.print(order, total); } main(); ``` ## Related Patterns - **OCP** — once responsibilities are separated (SRP), each class can be extended independently (OCP). - **Facade** — after applying SRP, a facade can re-unify the separated classes behind a simple interface. - **Service Layer** — the service layer applies SRP at the architectural level: orchestration logic is separated from domain logic and persistence. - **Mediator** — a mediator centralises coordination logic so individual components each have a single responsibility.