On this page

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

PrincipleSmell in the codeTypical fix
SRPA class that changes for many reasons, or needs "and" to describe itSplit it by reason to change
OCPA growing if-else or switch on a typeInterface + one class per variation
LSPAn override that throws or does nothing, or instanceof checksRedesign the hierarchy, split interfaces, or use composition
ISPClasses with empty methods forced by a fat interfaceSplit the interface into role-based ones
DIPnew for a database or service inside business codeDepend 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.

// Violation
public 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.

// Violation
public 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.

// Violation
public class Account {
public void withdraw(double amount) { /* reduce the balance */ }
}
public class FixedDepositAccount extends Account {
@Override
public void withdraw(double amount) {
throw new UnsupportedOperationException("Locked until maturity");
}
}
// Crashes when the list contains a FixedDepositAccount
public 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 FixedDepositAccount
for (Withdrawable account : accounts) {
account.withdraw(10);
}
}

Interface Segregation Principle (ISP)

Smell: a class implements methods it can never support.

// Violation
public 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.

// Violation
public class ReportService {
private PdfExporter exporter = new PdfExporter(); // glued to PDF
public 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 emailingSRP
Has a switch on type for each variationOCP
Creates its database with newDIP
Implements a fat interface with empty methodsISP

How to Fix Violations Safely

  1. Write tests first. Even a few tests around the current behavior let you refactor without fear.
  2. Fix one violation at a time. Do not rewrite the whole class in one go.
  3. Start with DIP and SRP. Injecting dependencies and splitting responsibilities make the code testable, which makes the other fixes easier.
  4. Refactor when you touch the code. Fix a violation in the area you are already changing, not in a big rewrite.
  5. 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 instanceof check 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 and instanceof (LSP), empty methods (ISP), and new for 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.

Next TopicDRY (Don't Repeat Yourself)