<arch.design/>
Principles/Single Responsibility Principle
{ }CodeArchitecturebeginner1999solidmartincohesionseparation-of-concerns

Single Responsibility Principle

A class should have only one reason to change — keep each unit focused on a single job so unrelated concerns don't accidentally break each other.

5/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

The Single Responsibility Principle (SRP) is the 'S' in SOLID, articulated by Robert C. Martin in 1999. It states that a module or class should have one, and only one, reason to change. 'Reason to change' maps to 'a stakeholder or a concern' — if two different actors or concerns could require changes to the same class, that class has two responsibilities.

The canonical violation: an OrderService that places orders, sends emails, and generates PDF invoices. Three reasons to change — every feature request from the email team, the PDF team, or the order team touches the same file, causing merge conflicts and unintended regressions.

SRP is about cohesion: grouping things that change together, and separating things that change for different reasons. A class that does too much is a symptom of missing domain concepts — each missing concept is a new class waiting to be extracted.

Implementation

TypeScript · Go · Rust
// ❌ Three reasons to change: order logic, email, and invoicing
class OrderService {
  placeOrder(order: Order): void {
    // ... validate and save order
    this.db.save(order);

    // Sending email has nothing to do with placing an order
    const html = `<h1>Thanks for your order #${order.id}</h1>`;
    this.mailer.send({ to: order.email, subject: "Order confirmed", html });

    // PDF generation is a third concern
    const pdf = this.pdfGen.render(order);
    this.storage.upload(`invoices/${order.id}.pdf`, pdf);
  }
}

// ✓ One reason to change per class
class OrderService {
  constructor(
    private repo: OrderRepository,
    private emailer: OrderEmailer,
    private invoicing: InvoiceService,
  ) {}

  async placeOrder(order: Order): Promise<void> {
    await this.repo.save(order);          // one concern
    await this.emailer.confirm(order);    // delegates, doesn't own
    await this.invoicing.generate(order); // delegates, doesn't own
  }
}

class OrderEmailer {
  async confirm(order: Order): Promise<void> { /* only email logic */ }
}

class InvoiceService {
  async generate(order: Order): Promise<void> { /* only PDF logic */ }
}

Why it matters

Classes with multiple responsibilities become magnets for unrelated change. Every new feature from every stakeholder lands in the same file. SRP keeps classes small, focused, and stable — a change to email sending cannot break order logic if they live in separate classes.

When to use

  • Always — apply SRP to every class, module, and function
  • When a class is growing too large or has too many imports
  • When merge conflicts repeatedly happen in the same file
  • When testing a class requires setting up unrelated infrastructure

When NOT to use

  • Over-splitting can create too many tiny classes that are hard to navigate — use judgment
  • In scripts and small utilities where a single file is genuinely simpler

Trade-offs

+

Changes to one concern cannot break other concerns

More classes means more files to navigate and more wiring

+

Classes are smaller and easier to understand and test

Finding which class owns a piece of logic requires knowledge of the system

+

Reduces merge conflicts in team settings

Premature decomposition before concerns are understood wastes time

In production

Rails / ActiveRecord

Fat model / skinny controller antipattern — ActiveRecord models accumulate too many responsibilities; service objects extract them

Spring Boot

@Service, @Repository, @Controller annotations enforce SRP by role — each layer has one job

Unix philosophy

'Do one thing and do it well' — each Unix tool has one responsibility; pipes compose them

Industry adoption

5/5Ubiquitous — used at virtually every scale-focused company.

Related principles