Solid
01 / 05

Single Responsibility Principle

Single Responsibility Principle (SRP)

"A class should have one, and only one, reason to change."

The Single Responsibility Principle states that a class should have only one job or responsibility. If a class has multiple responsibilities, it becomes coupled, making it harder to change and maintain.

The Problem: Violating SRP

// ❌ BAD: This class has multiple responsibilities
class Employee {
  constructor(
    public name: string,
    public email: string,
    public salary: number
  ) {}

  // Responsibility 1: Business logic
  calculateBonus(): number {
    return this.salary * 0.10;
  }

  // Responsibility 2: Database operations
  save(): void {
    const connection = openDatabaseConnection();
    connection.execute(
      `INSERT INTO employees VALUES ('${this.name}', '${this.email}', ${this.salary})`
    );
    connection.close();
  }

  // Responsibility 3: Email operations
  sendWelcomeEmail(): void {
    const smtp = new SMTPClient();
    smtp.connect();
    smtp.send(this.email, `Welcome ${this.name}!`);
    smtp.disconnect();
  }

  // Responsibility 4: Report generation
  generatePayslip(): string {
    return `
      PAYSLIP
      Name: ${this.name}
      Salary: $${this.salary}
      Bonus: $${this.calculateBonus()}
      Total: $${this.salary + this.calculateBonus()}
    `;
  }
}

// Why is this bad?
// 1. If database changes, we modify Employee class
// 2. If email system changes, we modify Employee class
// 3. If payslip format changes, we modify Employee class
// 4. Hard to test - need database and email setup for every test
// 5. Can't reuse email logic or database logic elsewhere

The Solution: Applying SRP

// ✅ GOOD: Each class has a single, well-defined responsibility

// Responsibility 1: Employee data and business logic
class Employee {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string,
    public salary: number
  ) {}

  calculateBonus(): number {
    return this.salary * 0.10;
  }

  getTotalCompensation(): number {
    return this.salary + this.calculateBonus();
  }
}

// Responsibility 2: Database operations
class EmployeeRepository {
  private db: Database;

  constructor(database: Database) {
    this.db = database;
  }

  save(employee: Employee): void {
    this.db.execute(
      `INSERT INTO employees (id, name, email, salary) 
       VALUES (?, ?, ?, ?)`,
      [employee.id, employee.name, employee.email, employee.salary]
    );
  }

  find(id: string): Employee | null {
    const result = this.db.query(
      `SELECT * FROM employees WHERE id = ?`,
      [id]
    );
    if (result.length === 0) return null;
    
    return new Employee(
      result[0].id,
      result[0].name,
      result[0].email,
      result[0].salary
    );
  }

  delete(id: string): void {
    this.db.execute(`DELETE FROM employees WHERE id = ?`, [id]);
  }
}

// Responsibility 3: Email operations
class EmailService {
  private smtp: SMTPClient;

  constructor(smtpConfig: SMTPConfig) {
    this.smtp = new SMTPClient(smtpConfig);
  }

  sendWelcomeEmail(employee: Employee): void {
    this.smtp.send(
      employee.email,
      'Welcome!',
      `Welcome to the company, ${employee.name}!`
    );
  }

  sendPayslipEmail(employee: Employee, payslip: string): void {
    this.smtp.send(
      employee.email,
      'Monthly Payslip',
      payslip
    );
  }
}

// Responsibility 4: Report generation
class PayslipGenerator {
  generate(employee: Employee): string {
    return `
      ═══════════════════════════════
              MONTHLY PAYSLIP
      ═══════════════════════════════
      Employee: ${employee.name}
      ID: ${employee.id}
      
      Base Salary:  $${employee.salary.toFixed(2)}
      Bonus:        $${employee.calculateBonus().toFixed(2)}
      ───────────────────────────────
      Total:        $${employee.getTotalCompensation().toFixed(2)}
      ═══════════════════════════════
    `;
  }
}

// Usage: Each class is focused and easy to test
const employee = new Employee('1', 'John Doe', 'john@example.com', 50000);

const repository = new EmployeeRepository(database);
repository.save(employee);

const emailService = new EmailService(smtpConfig);
emailService.sendWelcomeEmail(employee);

const payslipGen = new PayslipGenerator();
const payslip = payslipGen.generate(employee);
emailService.sendPayslipEmail(employee, payslip);

Benefits of SRP

  • Easier to understand: Each class does one thing, making code more readable

  • Easier to test: Can test each responsibility in isolation

  • Lower coupling: Classes are less dependent on each other

  • Easier to modify: Changes to one responsibility don't affect others

  • Better reusability: Focused classes can be reused in different contexts

Real-World Example: Order Processing

// ❌ BAD: Order class doing everything
class Order {
  processOrder() {
    // Validate order
    // Calculate total
    // Process payment
    // Update inventory
    // Send confirmation email
    // Generate invoice
    // Update analytics
  }
}

// ✅ GOOD: Separate responsibilities
class Order {
  constructor(
    public id: string,
    public items: OrderItem[],
    public customerId: string
  ) {}

  calculateTotal(): number {
    return this.items.reduce((sum, item) => sum + item.getPrice(), 0);
  }
}

class OrderValidator {
  validate(order: Order): ValidationResult {
    // Check if items are in stock
    // Validate customer exists
    // Check for minimum order amount
    return { isValid: true, errors: [] };
  }
}

class PaymentProcessor {
  process(order: Order, paymentMethod: PaymentMethod): PaymentResult {
    // Process payment through gateway
    return { success: true, transactionId: 'TX123' };
  }
}

class InventoryManager {
  updateStock(order: Order): void {
    order.items.forEach(item => {
      // Decrease stock for each item
    });
  }
}

class OrderNotificationService {
  sendConfirmation(order: Order): void {
    // Send confirmation email to customer
  }
}

class InvoiceGenerator {
  generate(order: Order): Invoice {
    // Generate PDF invoice
    return new Invoice(order);
  }
}

class AnalyticsTracker {
  trackOrderPlaced(order: Order): void {
    // Send event to analytics service
  }
}

// Coordinator that uses all services
class OrderProcessor {
  constructor(
    private validator: OrderValidator,
    private paymentProcessor: PaymentProcessor,
    private inventory: InventoryManager,
    private notifications: OrderNotificationService,
    private invoiceGen: InvoiceGenerator,
    private analytics: AnalyticsTracker
  ) {}

  async process(order: Order, payment: PaymentMethod): Promise<void> {
    const validation = this.validator.validate(order);
    if (!validation.isValid) {
      throw new Error('Order validation failed');
    }

    const paymentResult = await this.paymentProcessor.process(order, payment);
    if (!paymentResult.success) {
      throw new Error('Payment failed');
    }

    this.inventory.updateStock(order);
    this.notifications.sendConfirmation(order);
    this.invoiceGen.generate(order);
    this.analytics.trackOrderPlaced(order);
  }
}

How to Identify SRP Violations

  • Class name contains "and" (e.g., UserAndPermissionManager)

  • Class has too many methods (rule of thumb: >10 methods)

  • Class changes for multiple unrelated reasons

  • Difficult to name the class concisely

  • Class depends on many different libraries or frameworks

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free