OOP Concepts · Design Principles

Single Responsibility Principle

The S in SOLID: a class should have one job — more precisely, one reason to change. When a class calculates, prints, saves and emails, every kind of change lands in the same file.

Intermediate 5 min Lesson 23 of 38 Runnable examplesExamples use modern JavaScript (ES2022 private fields) and C# 13.
Before you start:SOLID Principles
0/38 in OOP Concepts
On this page · 5 min · Intermediate

In simple words#

A chef cooks; a cashier bills. If the chef also does billing and cleaning, one change affects everything. One class, one reason to change.

SRP
A class should have one reason to change.
Responsibility
A job or concern (e.g. saving, emailing).
Smell
Class names with “And” or “Manager” often do too much.

Learning objectives#

  • State SRP as “one reason to change”
  • Spot classes with too many responsibilities
  • Split a class into focused collaborators
  • Avoid splitting too far

Explanation#

A responsibility is a reason to change. If the tax rule changes, the PDF layout changes and the database changes all require editing Invoice, then Invoice has three responsibilities.

Mixing responsibilities causes collisions: a change to email formatting can break total calculation because they share one class and its state.

Fix it by moving each responsibility into its own class: Invoice (the data and calculation), InvoicePrinter (formatting), InvoiceRepository (storage), InvoiceMailer (sending).

Each class becomes small, easy to name, easy to test and safe to change.

Do not overdo it. A class with three tiny methods that always change together is fine. Split when changes come from different people or different reasons.

Syntax#

C# Illustrative
// ✗ One class, four reasons to change
// class Invoice { Total() ToPdf() Save() Email() }


// ✓ One reason each
public class Invoice(List<decimal> amounts)
{
    public decimal Total => amounts.Sum();
}
public class InvoicePrinter    { public string Print(Invoice i) => 
quot;Total:
{i.Total}"; }
public class InvoiceRepository { public void Save(Invoice i) { /* database */ } } public class InvoiceMailer { public void Send(Invoice i, string to) { /* email */ } }

Code example: Splitting a do-everything invoice#

JavaScriptSplitting a do-everything invoice Runnable
// Each class has exactly one reason to change.


class Invoice {                          // business rules
  constructor(items) { this.items = items; }
  get total() {
    return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
  }
}


class InvoicePrinter {                   // presentation
  print(invoice) {
    const lines = invoice.items.map(i => `${i.name} x${i.qty}`).join(', ');
    return `${lines} = ₹${invoice.total}`;
  }
}


class InvoiceRepository {                // storage
  #rows = [];
  save(invoice) { this.#rows.push(invoice); return this.#rows.length; }
}


class InvoiceMailer {                    // communication
  send(invoice, to) { return `Mailed invoice of ₹${invoice.total} to ${to}`; }
}


const invoice = new Invoice([
  { name: 'Pen', price: 20, qty: 3 },
  { name: 'Notebook', price: 60, qty: 2 },
]);


console.log(new InvoicePrinter().print(invoice));
console.log('Saved, row', new InvoiceRepository().save(invoice));
console.log(new InvoiceMailer().send(invoice, 'asha@example.com'));
Expected output
Pen x3, Notebook x2 = ₹180
Saved, row 1
Mailed invoice of ₹180 to asha@example.com
Opens this example in the full playground

Step-by-step explanation#

  1. Invoice only knows its items and how to total them.
  2. InvoicePrinter changes when the layout changes — and only then.
  3. InvoiceRepository changes when storage changes (SQL Server, files, an API).
  4. InvoiceMailer changes when the email provider or template changes.
  5. Each class can be tested alone with tiny, focused tests.

Real-world use case#

A controller that validates input, queries SQL, calculates discounts and builds an email is split into a validator, a repository, a pricing service and a notifier. The controller just coordinates them.

Common mistakes#

“Manager”, “Helper” and “Utils” classes

Vague names usually hide many responsibilities. If you cannot name a class precisely, it probably does too much.

Splitting into one-method classes everywhere

SRP is about reasons to change, not number of methods. Code that always changes together can stay together.

Best practices#

  • Describe each class in one sentence without “and”
  • Group code by reason to change
  • Keep controllers thin; move logic into services
  • Watch for classes over ~200 lines or with many dependencies

Remember this#

SRP = one reason to change.

Can’t describe it without “and”? Split it.

Calculate, present, store, send — four different jobs.

Practice exercise#

Your task: This class does two jobs: class Report { constructor(data) { this.data = data; } average() { ... } toCsv() { ... } }. Split it into Report (average) and ReportCsvFormatter (toCsv), then use both.

Need a hint?
The formatter takes a report as a parameter.
Show solution
JavaScript Runnable
class Report {
  constructor(data) { this.data = data; }
  average() { return this.data.reduce((a, b) => a + b, 0) / this.data.length; }
}
class ReportCsvFormatter {
  format(report) { return report.data.join(','); }
}
const r = new Report([10, 20, 30]);
console.log(r.average());                         // 20
console.log(new ReportCsvFormatter().format(r));  // 10,20,30
Opens this example in the full playground

Mini quiz#

Single Responsibility Principle quizMultiple choice Question 1 of 3

SRP says a class should have…

Score so far: 0/0

Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.