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.
On this page · 5 min · Intermediate
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
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#
// ✗ 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#
// 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'));
Pen x3, Notebook x2 = ₹180 Saved, row 1 Mailed invoice of ₹180 to asha@example.com
Step-by-step explanation#
- Invoice only knows its items and how to total them.
- InvoicePrinter changes when the layout changes — and only then.
- InvoiceRepository changes when storage changes (SQL Server, files, an API).
- InvoiceMailer changes when the email provider or template changes.
- 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#
Vague names usually hide many responsibilities. If you cannot name a class precisely, it probably does too much.
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?
Show solution
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
Mini quiz#
SRP says a class should have…
Want more? Practise all OOP Concepts MCQs · OOP Concepts interview questions
My notes#
Saved in this browser.