SOLID Violations and How to Fix Them
Knowing the five SOLID principles is one skill. Spotting a violation in real code and fixing it is the skill interviewers and code reviews actually test. This page shows the typical smell for each principle, a short before and after, and how to fix things safely.
Quick Cheat Sheet
| Principle | Smell in the code | Typical fix |
|---|---|---|
| SRP | A class that changes for many reasons, or needs "and" to describe it | Split it by reason to change |
| OCP | A growing if-else or switch on a type | Interface + one class per variation |
| LSP | An override that throws or does nothing, or instanceof checks | Redesign the hierarchy, split interfaces, or use composition |
| ISP | Classes with empty methods forced by a fat interface | Split the interface into role-based ones |
| DIP | new for a database or service inside business code | Depend on an interface and inject it |
Single Responsibility Principle (SRP)
Smell: one class validates, saves, and sends emails. Three different teams can force it to change.
// Violationpublic class UserService {public void register(User user) {if (!user.getEmail().contains("@")) {throw new IllegalArgumentException("Bad email"); // validation}// ...SQL to save the user... // storage// ...SMTP code to send a welcome email... // notification}}
Fix: give each job its own class, and let the service coordinate.
public class UserService {private final UserValidator validator;private final UserRepository repository;private final WelcomeEmailSender emailSender;public UserService(UserValidator validator, UserRepository repository,WelcomeEmailSender emailSender) {this.validator = validator;this.repository = repository;this.emailSender = emailSender;}public void register(User user) {validator.validate(user);repository.save(user);emailSender.send(user);}}
Open/Closed Principle (OCP)
Smell: every new variation means editing the same method.
// Violationpublic double shippingCost(String method, double weight) {if (method.equals("STANDARD")) {return weight * 5;} else if (method.equals("EXPRESS")) {return weight * 12;}return 0; // adding "SAME_DAY" means editing this method again}
Fix: put the changing part behind an interface.
public interface ShippingPolicy {double cost(double weight);}public class StandardShipping implements ShippingPolicy {public double cost(double weight) { return weight * 5; }}public class ExpressShipping implements ShippingPolicy {public double cost(double weight) { return weight * 12; }}// SameDayShipping is just one more class. Nothing existing changes.public double shippingCost(ShippingPolicy policy, double weight) {return policy.cost(weight);}
Liskov Substitution Principle (LSP)
Smell: a subclass refuses something its parent promised.
// Violationpublic class Account {public void withdraw(double amount) { /* reduce the balance */ }}public class FixedDepositAccount extends Account {@Overridepublic void withdraw(double amount) {throw new UnsupportedOperationException("Locked until maturity");}}// Crashes when the list contains a FixedDepositAccountpublic void collectFees(List<Account> accounts) {for (Account account : accounts) {account.withdraw(10);}}
Fix: only accounts that really can withdraw should promise it.
public abstract class Account {public abstract double balance();}public interface Withdrawable {void withdraw(double amount);}public class SavingsAccount extends Account implements Withdrawable {public double balance() { return 0; }public void withdraw(double amount) { /* reduce the balance */ }}public class FixedDepositAccount extends Account {public double balance() { return 0; } // no withdraw method at all}public void collectFees(List<Withdrawable> accounts) { // the compiler blocks a FixedDepositAccountfor (Withdrawable account : accounts) {account.withdraw(10);}}
Interface Segregation Principle (ISP)
Smell: a class implements methods it can never support.
// Violationpublic interface Machine {void print();void scan();void fax();}public class BasicPrinter implements Machine {public void print() { /* prints */ }public void scan() { throw new UnsupportedOperationException(); }public void fax() { throw new UnsupportedOperationException(); }}
Fix: split by role.
public interface Printer { void print(); }public interface Scanner { void scan(); }public interface FaxMachine { void fax(); }public class BasicPrinter implements Printer {public void print() { /* prints */ }}public class OfficeMachine implements Printer, Scanner, FaxMachine {public void print() { /* prints */ }public void scan() { /* scans */ }public void fax() { /* sends a fax */ }}
Dependency Inversion Principle (DIP)
Smell: business code creates its own low-level details with new.
// Violationpublic class ReportService {private PdfExporter exporter = new PdfExporter(); // glued to PDFpublic void publish(Report report) {exporter.export(report);}}
Fix: depend on an interface, and inject the real class from outside.
public interface ReportExporter {void export(Report report);}public class PdfExporter implements ReportExporter {public void export(Report report) { /* write a PDF */ }}public class ReportService {private final ReportExporter exporter;public ReportService(ReportExporter exporter) {this.exporter = exporter;}public void publish(Report report) {exporter.export(report);}}
Now a CSV exporter, or a fake exporter in a test, can be passed in without touching ReportService.
One Class, Many Violations
Real violations rarely come alone. A single messy class often breaks several principles at once, and fixing one helps the others.
| In the same "god class" | Principle broken |
|---|---|
| Does validation, saving, and emailing | SRP |
Has a switch on type for each variation | OCP |
Creates its database with new | DIP |
| Implements a fat interface with empty methods | ISP |
How to Fix Violations Safely
- Write tests first. Even a few tests around the current behavior let you refactor without fear.
- Fix one violation at a time. Do not rewrite the whole class in one go.
- Start with DIP and SRP. Injecting dependencies and splitting responsibilities make the code testable, which makes the other fixes easier.
- Refactor when you touch the code. Fix a violation in the area you are already changing, not in a big rewrite.
- Run the tests after each step, and commit often.
Common Mistakes
- Over-fixing. Do not add an interface for every class. Fix violations where change actually keeps happening.
- Splitting by method count. Split by reason to change, not by size.
- Hiding a violation. An
instanceofcheck or a method that silently does nothing covers up the problem and adds a new one. - Fixing without tests. The refactor itself can introduce bugs.
Summary
- Each principle has a recognizable smell: many reasons to change (SRP), a growing
switch(OCP), throwing overrides andinstanceof(LSP), empty methods (ISP), andnewfor details (DIP). - The fixes are usually splitting a class, adding an interface, redesigning the hierarchy, or injecting a dependency.
- A messy class often breaks several principles at once.
- Refactor in small steps, with tests, and only where change keeps happening.
Interview Answer: How Do You Spot and Fix SOLID Violations?
I look for the typical smells. A class that changes for many reasons or needs "and" to describe it breaks SRP, and I split it. A growing if-else or switch on a type breaks OCP, and I replace it with an interface and one class per variation. Overrides that throw an exception or instanceof checks point to an LSP problem, and I fix the hierarchy by splitting interfaces or using composition. Empty methods forced by a big interface break ISP, and I split the interface by role. new for a database or service inside business code breaks DIP, and I depend on an interface and inject it.
I refactor in small steps, with tests first, and I start with DIP and SRP because they make the code testable. I only apply a fix where change actually keeps happening, so I do not over-engineer with unneeded interfaces.