Low-level design2 min

Single Responsibility Principle

The Single Responsibility Principle (SRP) states that a class should have one, and only one, reason to change.

This means a class should only have one primary responsibility or job. If a class has multiple responsibilities, it becomes tightly coupled. A change to one responsibility might break the code related to the other responsibility.

Real-Life Analogy

Think of a Swiss Army Knife. It's great for camping because it has a knife, a screwdriver, a corkscrew, and scissors. However, if the scissors break, you have to send the entire tool in for repair, leaving you without your knife and screwdriver.

In a professional kitchen, a chef doesn't use a Swiss Army Knife. They use a dedicated Chef's Knife for chopping, and a dedicated Corkscrew for wine. Each tool has a Single Responsibility.

Example: Violation of SRP

Here is a User class that handles user data, but also handles database saving and sending emails. It has three reasons to change!

typescript
class User {
  private name: string;
  private email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  // Reason to change 1: Business logic changes
  public changeEmail(newEmail: string): void {
    this.email = newEmail;
  }

  // Reason to change 2: Database schema or ORM changes
  public saveToDatabase(): void {
    console.log(`Saving ${this.name} to the database...`);
  }

  // Reason to change 3: Email provider (e.g. SendGrid) changes
  public sendWelcomeEmail(): void {
    console.log(`Sending welcome email to ${this.email}...`);
  }
}

Example: Proper SRP Design

We refactor by splitting the responsibilities into three distinct classes:

typescript
// 1. Core Data (Only changes if user properties change)
class User {
  public name: string;
  public email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}

// 2. Database Logic (Only changes if DB changes)
class UserRepository {
  public save(user: User): void {
    console.log(`Saving ${user.name} to the database...`);
  }
}

// 3. Email Logic (Only changes if Email provider changes)
class EmailService {
  public sendWelcomeEmail(user: User): void {
    console.log(`Sending welcome email to ${user.email}...`);
  }
}

Why is it Important?

  • Easier Maintenance: Bugs are isolated. Changing email logic won't accidentally break database logic.
  • Better Testing: You can easily mock the EmailService when testing the UserRepository.
  • Readability: Classes stay small and focused.