The code
package SOLID.SRP.GoodCode;
// Invoice.java: owns invoice calculation and presentation rules.
public class Invoice {
private double amount;
public Invoice(double amount) {
this.amount = amount;
}
public void generateInvoice() {
System.out.println("Invoice generated & printed for amount " + amount);
}
}
// InvoiceRepository.java: owns database storage details.
public class InvoiceRepository {
public void saveToDatabase() {
System.out.println("Saving invoice to database");
}
}
// EmailService.java: owns email delivery details.
public class EmailService {
public void sendEmailNotification() {
System.out.println("Sending email notification for invoice");
}
}Main now uses one class for each job:
Invoice invoice = new Invoice(100.0);
invoice.generateInvoice();
InvoiceRepository repository = new InvoiceRepository();
repository.saveToDatabase();
EmailService emailService = new EmailService();
emailService.sendEmailNotification();These are separate objects because storage and email are separate jobs. The calls happen in the same order as before. What changed is where each kind of knowledge lives.
What got better
Each class now answers to only one actor (one group of people who ask for its changes):
| Class | Changes when | Actor |
|---|---|---|
Invoice | tax or billing rules change | Finance |
InvoiceRepository | storage or schema changes | Platform / DBA |
EmailService | email provider or template changes | CRM / Marketing |
- Changes stay in one place. Switching from SMTP to SES (two ways to send email) touches only
EmailService. - Tests stay small.
new Invoice(499.0)needs no database and no mail server. - Bugs are easier to find. A storage failure points to the repository, not to one class that does everything.
- Classes can be reused on their own. You can use invoice calculation without dragging in email code.
We split by reasons to change, not by a rule like "one method per class." Invoice could have several methods, such as subtotal(), tax() and total(), and still follow SRP if finance owns all those rules.
What you would add in real code
This example keeps the method signatures tiny so the split is easy to see. Real code would usually do more:
- Pass the invoice to the other classes. The repo's
saveToDatabase()andsendEmailNotification()take no arguments, so they never see the invoice. A real version might look likerepository.save(invoice)andemailService.send(invoice, "ravi@example.com"). Those two method names are only examples, not in the repo. - Add a coordinator such as
BillingService.bill(invoice, email)to run the steps in order. It does not calculate, save or send anything itself. Its one job is the billing workflow. - Use a type that is safe for money, such as
BigDecimal, or store paise as along, instead ofdouble. - Put email or saving behind an interface when there is a real second implementation, or when tests need a fake. That idea is DIP.
There is a trade-off. To follow one flow you now open several files. That is worth it when different actors really change the parts on their own. If one small team always changes and tests all three together, splitting right away may only add clutter.
Compare with BadCode, and read SRP-Notes.md for more examples.