> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Single Responsibility Principle

> One class, one job - like workers in a factory!

## 🎯 The SRP Rule

> **"A class should have only ONE reason to change."**

Think of it like workers in a restaurant:

* 👨‍🍳 **Chef** - Only cooks food
* 🍽️ **Waiter** - Only serves customers
* 💰 **Cashier** - Only handles payments
* 🧹 **Cleaner** - Only cleans tables

If the chef also had to serve, clean, AND handle money, one bad day could mess up EVERYTHING!

> [!TIP]
>
> **Simple Rule**: If you describe your class and use the word "AND", you might be breaking SRP!
>
> ❌ "This class saves users AND sends emails AND logs activity"
> ✅ "This class saves users"

**The hospital analogy:** In a hospital, a surgeon operates, a pharmacist dispenses medication, and a radiologist reads scans. They all work on the same patient, but each has a distinct responsibility. If the surgeon also had to compound medications, a change in pharmaceutical regulations would force the surgeon to retrain -- even though it has nothing to do with surgery. SRP works the same way: when one class has multiple responsibilities, a change in one area forces modifications that ripple into unrelated areas.

***

## 🚨 Spotting SRP Violations

### ❌ BAD: The God Class

```python theme={null}
class UserManager:
    """This class does EVERYTHING related to users... and more!"""
    
    def __init__(self, db_connection):
        self.db = db_connection
    
    # Responsibility 1: User data operations
    def create_user(self, name, email, password):
        hashed_pw = self._hash_password(password)
        self.db.execute(
            "INSERT INTO users VALUES (?, ?, ?)",
            (name, email, hashed_pw)
        )
    
    def get_user(self, user_id):
        return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
    
    def update_user(self, user_id, data):
        # ... database update logic
        pass
    
    # Responsibility 2: Authentication (DIFFERENT JOB!)
    def _hash_password(self, password):
        import hashlib
        return hashlib.sha256(password.encode()).hexdigest()
    
    def verify_password(self, user_id, password):
        user = self.get_user(user_id)
        return user['password'] == self._hash_password(password)
    
    # Responsibility 3: Email sending (DIFFERENT JOB!)
    def send_welcome_email(self, user_email):
        import smtplib
        # Complex email sending logic...
        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.send_message(f"Welcome to our app!")
    
    def send_password_reset(self, user_email):
        # More email logic...
        pass
    
    # Responsibility 4: Logging (DIFFERENT JOB!)
    def log_activity(self, user_id, action):
        with open('user_activity.log', 'a') as f:
            f.write(f"{user_id}: {action}\n")
    
    # Responsibility 5: Report generation (DIFFERENT JOB!)
    def generate_user_report(self):
        users = self.db.query("SELECT * FROM users")
        return f"Total users: {len(users)}"

# 😱 This class has 5 reasons to change:
# 1. If database schema changes
# 2. If password hashing algorithm changes
# 3. If email provider changes
# 4. If logging format changes
# 5. If report format changes
```

***

### ✅ GOOD: Focused Classes

```python theme={null}
# Each class has ONE job!

class UserRepository:
    """ONLY handles user data storage"""
    
    def __init__(self, db_connection):
        self.db = db_connection
    
    def create(self, user_data):
        self.db.execute("INSERT INTO users ...", user_data)
    
    def find_by_id(self, user_id):
        return self.db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    def update(self, user_id, data):
        self.db.execute("UPDATE users SET ...", data)
    
    def delete(self, user_id):
        self.db.execute("DELETE FROM users WHERE id = ?", user_id)


class PasswordService:
    """ONLY handles password operations"""
    
    def hash(self, password):
        import hashlib
        return hashlib.sha256(password.encode()).hexdigest()
    
    def verify(self, password, hashed):
        return self.hash(password) == hashed


class EmailService:
    """ONLY handles email sending"""
    
    def __init__(self, smtp_config):
        self.config = smtp_config
    
    def send(self, to, subject, body):
        # All email logic here
        print(f"📧 Sending '{subject}' to {to}")
    
    def send_welcome(self, user_email):
        self.send(user_email, "Welcome!", "Thanks for joining!")
    
    def send_password_reset(self, user_email, reset_link):
        self.send(user_email, "Reset Password", f"Click: {reset_link}")


class ActivityLogger:
    """ONLY handles logging"""
    
    def __init__(self, log_file):
        self.log_file = log_file
    
    def log(self, user_id, action):
        with open(self.log_file, 'a') as f:
            from datetime import datetime
            timestamp = datetime.now().isoformat()
            f.write(f"[{timestamp}] User {user_id}: {action}\n")


class UserReportGenerator:
    """ONLY generates reports"""
    
    def __init__(self, user_repository):
        self.repo = user_repository
    
    def generate_summary(self):
        users = self.repo.find_all()
        return {
            "total": len(users),
            "active": sum(1 for u in users if u.is_active)
        }


# DESIGN REASONING: Now each class has ONE reason to change.
# If the email provider switches from SMTP to SendGrid, only
# EmailService changes. If the database schema evolves, only
# UserRepository changes. The blast radius of any change is
# contained to a single class.

# The UserService coordinates them (this is called a "Facade"):
class UserService:
    """Coordinates user operations using focused services.
    This class orchestrates but does not implement any single concern."""
    
    def __init__(self):
        self.user_repo = UserRepository(db_connection)
        self.password_service = PasswordService()
        self.email_service = EmailService(smtp_config)
        self.logger = ActivityLogger('activity.log')
    
    def register_user(self, name, email, password):
        # Each service handles its own concern -- UserService just choreographs
        hashed_pw = self.password_service.hash(password)
        user = self.user_repo.create({'name': name, 'email': email, 'password': hashed_pw})
        self.email_service.send_welcome(email)
        self.logger.log(user.id, 'registered')
        return user
```

***

## 🎮 Real Example: Invoice System

Let's fix an invoice system step by step:

### ❌ Before: One Class Does Everything

```python theme={null}
class Invoice:
    def __init__(self, items):
        self.items = items
    
    # Responsibility 1: Calculate totals
    def calculate_total(self):
        return sum(item.price * item.quantity for item in self.items)
    
    def calculate_tax(self):
        return self.calculate_total() * 0.1
    
    # Responsibility 2: Format/Print (DIFFERENT JOB!)
    def print_invoice(self):
        print("=" * 40)
        print("INVOICE")
        print("=" * 40)
        for item in self.items:
            print(f"{item.name}: ${item.price} x {item.quantity}")
        print(f"Total: ${self.calculate_total()}")
        print(f"Tax: ${self.calculate_tax()}")
    
    def export_to_pdf(self):
        # PDF generation logic...
        pass
    
    def export_to_excel(self):
        # Excel generation logic...
        pass
    
    # Responsibility 3: Save to database (DIFFERENT JOB!)
    def save_to_database(self, db):
        db.execute("INSERT INTO invoices ...")
    
    # Responsibility 4: Send via email (DIFFERENT JOB!)
    def email_to_customer(self, email):
        # Email sending logic...
        pass
```

### ✅ After: Each Class Has One Job

```python theme={null}
from dataclasses import dataclass
from typing import List

@dataclass
class InvoiceItem:
    name: str
    price: float
    quantity: int

class Invoice:
    """ONLY holds invoice data and calculations"""
    
    def __init__(self, invoice_id: str, items: List[InvoiceItem]):
        self.id = invoice_id
        self.items = items
    
    def calculate_subtotal(self):
        return sum(item.price * item.quantity for item in self.items)
    
    def calculate_tax(self, tax_rate=0.1):
        return self.calculate_subtotal() * tax_rate
    
    def calculate_total(self, tax_rate=0.1):
        return self.calculate_subtotal() + self.calculate_tax(tax_rate)


class InvoicePrinter:
    """ONLY handles printing"""
    
    def print_to_console(self, invoice: Invoice):
        print("╔════════════════════════════════════════╗")
        print(f"║  INVOICE #{invoice.id}")
        print("╠════════════════════════════════════════╣")
        for item in invoice.items:
            line = f"  {item.name}: ${item.price:.2f} x {item.quantity}"
            print(f"║{line:<39}║")
        print("╠════════════════════════════════════════╣")
        print(f"║  Subtotal: ${invoice.calculate_subtotal():>26.2f}║")
        print(f"║  Tax:      ${invoice.calculate_tax():>26.2f}║")
        print(f"║  TOTAL:    ${invoice.calculate_total():>26.2f}║")
        print("╚════════════════════════════════════════╝")


class InvoiceExporter:
    """ONLY handles exporting to different formats"""
    
    def to_pdf(self, invoice: Invoice, filename: str):
        print(f"📄 Exporting invoice {invoice.id} to {filename}.pdf")
        # PDF generation logic
    
    def to_excel(self, invoice: Invoice, filename: str):
        print(f"📊 Exporting invoice {invoice.id} to {filename}.xlsx")
        # Excel generation logic
    
    def to_json(self, invoice: Invoice) -> str:
        import json
        return json.dumps({
            'id': invoice.id,
            'items': [vars(item) for item in invoice.items],
            'total': invoice.calculate_total()
        })


class InvoiceRepository:
    """ONLY handles database operations"""
    
    def __init__(self, database):
        self.db = database
    
    def save(self, invoice: Invoice):
        print(f"💾 Saving invoice {invoice.id} to database")
        # Database save logic
    
    def find_by_id(self, invoice_id: str) -> Invoice:
        print(f"🔍 Finding invoice {invoice_id}")
        # Database query logic
        pass


class InvoiceEmailer:
    """ONLY handles email sending"""
    
    def __init__(self, email_service):
        self.email_service = email_service
    
    def send_to_customer(self, invoice: Invoice, customer_email: str):
        print(f"📧 Emailing invoice {invoice.id} to {customer_email}")
        # Email sending logic


# 🎉 Usage - clear separation of concerns!
items = [
    InvoiceItem("Widget", 10.00, 5),
    InvoiceItem("Gadget", 25.00, 2),
]

invoice = Invoice("INV-001", items)

# Each service does its ONE job
printer = InvoicePrinter()
printer.print_to_console(invoice)

exporter = InvoiceExporter()
exporter.to_pdf(invoice, "invoice_001")

# Easy to swap, test, or modify each piece!
```

***

## 🧪 How to Check for SRP Violations

Use these questions:

> **SRP Checklist**
>
> 1. **Can you describe the class in one sentence WITHOUT using "and"?**
>    * ✅ "This class stores user data"
>    * ❌ "This class stores user data AND sends emails"
> 2. **If you need to change one feature, do you touch multiple unrelated methods?**
>    * ✅ Changing email format only touches EmailService
>    * ❌ Changing email format requires editing UserManager
> 3. **How many reasons could this class change?**
>    * ✅ One reason (e.g., business rules for orders)
>    * ❌ Multiple reasons (email format, database schema, logging format...)
> 4. **Could you easily test this class in isolation?**
>    * ✅ UserRepository can be tested with just a mock database
>    * ❌ UserManager needs mock database, mock email server, mock logger...
> 5. **Would a team member understand the class purpose immediately?**
>    * ✅ "InvoicePrinter" - obviously prints invoices
>    * ❌ "InvoiceManager" - does it manage? print? email? save?

***

## 💡 Common SRP Violations & Fixes

| Violation | Problem | Fix |
| - | - | - |
| **God Class** | Does everything | Split into focused classes |
| **Utility Class** | Bag of random methods | Group by purpose into services |
| **Active Record** | Data + Database + Validation | Separate into Entity + Repository |
| **Fat Controller** | HTTP + Business Logic + Data | Use Service Layer pattern |

***

## 🏋️ Practice Exercise

> **Challenge: Fix the BookStore Class**
>
> This class violates SRP. Split it into focused classes:
>
> ```python theme={null}
> class BookStore:
>     def __init__(self):
>         self.books = []
>         self.customers = []
>         self.orders = []
>
>     # Data operations
>     def add_book(self, book): pass
>     def remove_book(self, book_id): pass
>     def find_book(self, title): pass
>
>     # Customer operations  
>     def register_customer(self, customer): pass
>     def update_customer(self, customer_id, data): pass
>
>     # Order operations
>     def create_order(self, customer_id, book_ids): pass
>     def cancel_order(self, order_id): pass
>
>     # Inventory
>     def check_stock(self, book_id): pass
>     def restock(self, book_id, quantity): pass
>
>     # Reports
>     def generate_sales_report(self): pass
>     def generate_inventory_report(self): pass
>
>     # Notifications
>     def send_order_confirmation(self, order_id): pass
>     def send_shipping_update(self, order_id, status): pass
> ```
>
> <details>
>   <summary>Click for Solution</summary>
>
>   ```python theme={null}
>   # ✅ Split into focused classes:
>
>   class BookRepository:
>       """ONLY manages book data"""
>       def add(self, book): pass
>       def remove(self, book_id): pass
>       def find(self, title): pass
>       def find_all(self): pass
>
>
>   class CustomerRepository:
>       """ONLY manages customer data"""
>       def register(self, customer): pass
>       def update(self, customer_id, data): pass
>       def find(self, customer_id): pass
>
>
>   class OrderService:
>       """ONLY handles order logic"""
>       def __init__(self, book_repo, customer_repo, inventory):
>           self.books = book_repo
>           self.customers = customer_repo
>           self.inventory = inventory
>
>       def create(self, customer_id, book_ids): pass
>       def cancel(self, order_id): pass
>
>
>   class InventoryService:
>       """ONLY manages stock levels"""
>       def check_stock(self, book_id): pass
>       def restock(self, book_id, quantity): pass
>       def decrease_stock(self, book_id, quantity): pass
>
>
>   class ReportService:
>       """ONLY generates reports"""
>       def sales_report(self, orders): pass
>       def inventory_report(self, inventory): pass
>
>
>   class NotificationService:
>       """ONLY sends notifications"""
>       def send_order_confirmation(self, order): pass
>       def send_shipping_update(self, order, status): pass
>
>
>   # 🎉 Now we have 6 focused classes instead of 1 monster!
>   # Each can be tested, modified, and understood independently.
>   ```
> </details>

***

## 📝 Key Takeaways

> **One Job Only**
>
> Each class should have only ONE reason to change

> **No 'AND' Description**
>
> If you use "and" to describe it, split it up

> **Easy to Test**
>
> Focused classes are easy to test in isolation

> **Easy to Name**
>
> Good names describe exactly what the class does

***

## Interview Insight

> [!NOTE]
>
> **SRP in LLD interviews:** When you present a class diagram, interviewers mentally check if each class has a single, clear responsibility. The most common mistake candidates make is creating a "God class" -- a `BookingManager` that handles reservation logic, payment processing, email notifications, and database persistence all in one. Instead, separate these into `ReservationService`, `PaymentService`, `NotificationService`, and `BookingRepository`. When the interviewer asks "what happens if we need to switch from email to push notifications?", you can say "only the NotificationService changes." That one sentence demonstrates SRP understanding. The rule of thumb: if a class name ends in "Manager" or "Handler" and has more than 5-6 methods, it probably violates SRP.

***

## 🏃 Next: Open/Closed Principle

Now that your classes are focused, let's learn how to add features WITHOUT changing existing code!

> **[Continue to Open/Closed Principle →](https://resources.devweekends.com/lld/solid/ocp.md)**
>
> Learn how to extend your code without modifying it!


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.