Single Responsibility Principle (SRP)
What is it? (The Analogy)
Imagine you have a friend named Alex who works at a restaurant. Alex is the chef, the waiter, the cashier, AND the janitor -- all at the same time. On a quiet Tuesday, Alex manages. But on a busy Friday night? Total chaos. Orders get mixed up, tables are dirty, the food burns, and customers are overcharged. Why? Because Alex is doing too many jobs at once.
Now imagine the restaurant hires separate people for each role. The chef only cooks. The waiter only serves. The cashier only handles payments. The janitor only cleans. Each person has one job, and they do it well. If the chef calls in sick, you replace the chef -- you don't need to retrain someone who also knows how to mop floors and run the cash register.
That's the Single Responsibility Principle in a nutshell: every class should have one -- and only one -- reason to change. Just like every employee at that restaurant should have one clear job.
Why do we need it?
Let's feel the pain first. Here's a class that does WAY too much:
1// BAD EXAMPLE -- This class has TOO MANY responsibilities!
2public class Student {
3 private String name;
4 private int age;
5 private List<Integer> grades;
6
7 public Student(String name, int age) {
8 this.name = name;
9 this.age = age;
10 this.grades = new ArrayList<>();
11 }
12
13 // Responsibility 1: Managing student data
14 public void addGrade(int grade) {
15 grades.add(grade);
16 }
17
18 // Responsibility 2: Calculating statistics (business logic)
19 public double calculateAverageGrade() {
20 return grades.stream()
21 .mapToInt(Integer::intValue)
22 .average()
23 .orElse(0.0);
24 }
25
26 // Responsibility 3: Generating reports (formatting/display)
27 public String generateReportCard() {
28 return "Report Card for " + name + "\n"
29 + "Age: " + age + "\n"
30 + "Average: " + calculateAverageGrade() + "\n"
31 + "Grades: " + grades.toString();
32 }
33
34 // Responsibility 4: Saving to database (persistence)
35 public void saveToDatabase() {
36 // Database connection code here...
37 System.out.println("Saving " + name + " to database...");
38 }
39
40 // Responsibility 5: Sending emails (notification)
41 public void emailReportToParents(String parentEmail) {
42 // Email sending code here...
43 System.out.println("Emailing report to " + parentEmail);
44 }
45}What's wrong with this? Everything is tangled together. If you want to change how reports look, you touch the Student class. If the database changes, you touch the Student class. If you switch email providers, you touch the Student class. Every change risks breaking something unrelated.
The pain: When one class does five things, a bug fix for one feature can break four others. It's like pulling one thread and watching the whole sweater unravel.
How it works -- Step by Step
- Identify the responsibilities -- Look at your class and ask: "How many reasons could this class change?" Each reason = one responsibility.
- Group related things together -- Things that change for the same reason belong together. Things that change for different reasons belong apart.
- Extract each responsibility into its own class -- Create a new class for each responsibility. Give it a clear, descriptive name.
- Wire them together -- Use composition (passing objects to each other) so the classes can still work together without being tangled.
- Test the "newspaper headline" rule -- Can you describe what your class does in ONE sentence without using the word "and"? If not, it probably has too many responsibilities.
Let's Build It Together
Let's refactor our Student example into clean, single-responsibility classes. We'll use a Pizza Shop theme to make it fun.
1// ========================================
2// Step 1: The core data class -- just holds pizza order info
3// ========================================
4public class PizzaOrder {
5 private String customerName;
6 private String pizzaType;
7 private String size;
8 private double price;
9
10 // Constructor -- just sets up the data
11 public PizzaOrder(String customerName, String pizzaType, String size, double price) {
12 this.customerName = customerName;
13 this.pizzaType = pizzaType;
14 this.size = size;
15 this.price = price;
16 }
17
18 // Only getters and setters -- this class ONLY manages order data
19 public String getCustomerName() { return customerName; }
20 public String getPizzaType() { return pizzaType; }
21 public String getSize() { return size; }
22 public double getPrice() { return price; }
23}1// ========================================
2// Step 2: A separate class just for price calculations
3// ========================================
4public class PizzaPriceCalculator {
5
6 // This class has ONE job: figure out the price
7 public double calculateTotal(PizzaOrder order, double taxRate) {
8 double subtotal = order.getPrice();
9 double tax = subtotal * taxRate;
10 return subtotal + tax;
11 }
12
13 public double applyDiscount(double total, double discountPercent) {
14 return total * (1 - discountPercent / 100);
15 }
16}1// ========================================
2// Step 3: A separate class just for generating receipts
3// ========================================
4public class PizzaReceiptPrinter {
5
6 // This class has ONE job: format and print receipts
7 public String generateReceipt(PizzaOrder order, double total) {
8 StringBuilder receipt = new StringBuilder();
9 receipt.append("================================\n");
10 receipt.append(" MARIO'S PIZZA PALACE\n");
11 receipt.append("================================\n");
12 receipt.append("Customer: ").append(order.getCustomerName()).append("\n");
13 receipt.append("Pizza: ").append(order.getPizzaType()).append("\n");
14 receipt.append("Size: ").append(order.getSize()).append("\n");
15 receipt.append("Total: $").append(String.format("%.2f", total)).append("\n");
16 receipt.append("================================\n");
17 return receipt.toString();
18 }
19}1// ========================================
2// Step 4: A separate class just for saving orders
3// ========================================
4public class PizzaOrderRepository {
5
6 // This class has ONE job: save and load orders
7 public void save(PizzaOrder order) {
8 System.out.println("Saving order for " + order.getCustomerName() + " to database...");
9 // Real database code would go here
10 }
11
12 public PizzaOrder findByCustomerName(String name) {
13 System.out.println("Looking up order for " + name + "...");
14 // Real database query would go here
15 return null;
16 }
17}1// ========================================
2// Step 5: Wire it all together!
3// ========================================
4public class PizzaShopApp {
5 public static void main(String[] args) {
6 // Create an order (data only)
7 PizzaOrder order = new PizzaOrder("Alice", "Margherita", "Large", 12.99);
8
9 // Calculate the price (separate responsibility)
10 PizzaPriceCalculator calculator = new PizzaPriceCalculator();
11 double total = calculator.calculateTotal(order, 0.08); // 8% tax
12 total = calculator.applyDiscount(total, 10); // 10% discount
13
14 // Print the receipt (separate responsibility)
15 PizzaReceiptPrinter printer = new PizzaReceiptPrinter();
16 System.out.println(printer.generateReceipt(order, total));
17
18 // Save to database (separate responsibility)
19 PizzaOrderRepository repo = new PizzaOrderRepository();
20 repo.save(order);
21 }
22}Aha moment: Notice how each class can be changed independently. Want to switch from a database to a file? Only PizzaOrderRepository changes. Want a fancier receipt? Only PizzaReceiptPrinter changes. The other classes don't even know or care!
Visual Mental Model
1 BEFORE (everything tangled): AFTER (clean separation):
2
3 +---------------------------+ +------------------+
4 | PizzaOrder | | PizzaOrder |
5 |---------------------------| | (data only) |
6 | - customerName | +------------------+
7 | - pizzaType | |
8 | - price | +----------+----------+----------+
9 |---------------------------| | | | |
10 | + calculateTotal() | v v v v
11 | + applyDiscount() | +--------+ +--------+ +--------+ +--------+
12 | + generateReceipt() | | Price | |Receipt | | Order | | Notify |
13 | + saveToDatabase() | | Calc | |Printer | | Repo | |Service |
14 | + notifyCustomer() | +--------+ +--------+ +--------+ +--------+
15 +---------------------------+ 1 job 1 job 1 job 1 job
16 5 reasons to change! each! each! each! each!Common Mistakes & Gotchas
- Going too far -- Don't create a class for every single method. A class with one method that never grows is overkill. Group things that change for the same reason.
- Confusing "responsibility" with "method" -- A responsibility is a reason to change, not a single function. A
PriceCalculatormight havecalculateTotal(),applyDiscount(), andapplyCoupon()-- they all relate to pricing, so they belong together. - God classes -- If your class name is something vague like
Manager,Handler, orUtils, it's probably doing too much. Good class names are specific:ReceiptPrinter,OrderValidator,PriceCalculator. - Not recognizing hidden responsibilities -- Logging, validation, formatting, and persistence are all separate responsibilities that often sneak into business logic classes.
- Ignoring the principle for "small" projects -- Even small projects grow. Starting with SRP makes future changes painless.
Interview Tip
Interviewers love asking: "How would you refactor this class?" The SRP is almost always relevant. When you see a big class in an interview, identify the responsibilities out loud: "I see this class is doing data storage, validation, and formatting -- I'd split those into three classes." This shows design maturity. Also, mention that SRP makes testing easier -- you can unit test each class independently.
Quick Quiz
- You have a
UserProfileclass that stores user data, validates email format, hashes passwords, and sends welcome emails. How many responsibilities does it have? Which classes would you create?
- A teammate argues: "SRP means every class should have only one method." Are they right? Why or why not?
- You're building a game. Your
Playerclass handles movement, health, inventory, and rendering on screen. If you apply SRP, what classes might you end up with?
Summary -- Key Takeaways
- One class, one reason to change. If you can think of multiple unrelated reasons a class might need to be modified, it has too many responsibilities.
- Split by reason for change, not by number of methods. Related methods that change together should stay together.
- SRP makes code easier to test, easier to understand, and safer to change. When classes are focused, bugs stay contained and fixes stay small.
- Use the "newspaper headline" test: Can you describe your class's purpose in one sentence without "and"? If not, split it up.