Skip to main content

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.

🎯 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!
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

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

# 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

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

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:
  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

ViolationProblemFix
God ClassDoes everythingSplit into focused classes
Utility ClassBag of random methodsGroup by purpose into services
Active RecordData + Database + ValidationSeparate into Entity + Repository
Fat ControllerHTTP + Business Logic + DataUse Service Layer pattern

🏋️ Practice Exercise

This class violates SRP. Split it into focused classes:
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

📝 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

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 →

Learn how to extend your code without modifying it!