Menu

SOLID Principles in Java

SOLID Principles in Java

SOLID was introduced briefly at the Beginner level as a list of five names. Here, each principle gets a proper, code-level treatment, with a concrete 'before' example that violates it and an 'after' example that fixes it, since these principles are far easier to internalise by seeing the actual problem they solve.

Single Responsibility Principle

A class should have exactly one reason to change. When a class takes on two unrelated responsibilities, a change driven by one of them risks breaking the other, even though the two were never logically connected.

// VIOLATES SRP   Invoice handles both business data AND how it's persisted

class Invoice {

double amount;

double calculateTotal() {

return amount * 1.18;   // business logic

}

void saveToDatabase() {   // persistence logic — a completely different concern

System.out.println("Saving invoice to database...");

}

}

// FOLLOWS SRP   each class has exactly one reason to change

class Invoice {

double amount;

double calculateTotal() {

return amount * 1.18;

}

}

class InvoiceRepository {

void save(Invoice invoice) {

System.out.println("Saving invoice to database...");

}

}

Open/Closed Principle

Classes should be open for extension, but closed for modification. You should be able to add new behaviour by writing new code, not by editing and risking existing, already-tested code every time a new requirement appears.

// VIOLATES OCP   adding a new shape means editing this method's if-chain every time

class AreaCalculator {

double calculate(Object shape) {

if (shape instanceof Circle c) {

return Math.PI * c.radius * c.radius;

} else if (shape instanceof Rectangle r) {

return r.width * r.height;

}

return 0;   // every new shape requires editing this method again

}

}

// FOLLOWS OCP — new shapes extend Shape; AreaCalculator never changes again

interface Shape {

double area();

}

class Circle implements Shape {

double radius;

Circle(double r) { radius = r; }

public double area() { return Math.PI * radius * radius; }

}

class Rectangle implements Shape {

double width, height;

Rectangle(double w, double h) { width = w; height = h; }

public double area() { return width * height; }

}

class AreaCalculator {

double calculate(Shape shape) {

return shape.area();   // works for every current AND future Shape, unmodified

}

}

Liskov Substitution Principle

Objects of a subclass should be substitutable for objects of the superclass without breaking the program's correctness. The classic violation is the 'Square extends Rectangle' problem, an inheritance relationship that looks correct geometrically but breaks behaviourally.

// VIOLATES LSP — a Square forced to behave like a Rectangle breaks expectations

class Rectangle {

protected double width, height;

void setWidth(double w) { width = w; }

void setHeight(double h) { height = h; }

double area() { return width * height; }

}

class Square extends Rectangle {

@Override

void setWidth(double w) { width = w; height = w; }   // surprising side effect!

@Override

void setHeight(double h) { width = h; height = h; }

}

Rectangle r = new Square();

r.setWidth(5);

r.setHeight(10);

System.out.println(r.area());   // 100, not 50 — silently broke caller expectations

// FOLLOWS LSP — Square and Rectangle are siblings, not parent/child

interface Shape {

double area();

}

class Rectangle implements Shape {

protected double width, height;

Rectangle(double w, double h) { width = w; height = h; }

public double area() { return width * height; }

}

class Square implements Shape {

private double side;

Square(double s) { side = s; }

public double area() { return side * side; }

}

Interface Segregation and Dependency Inversion

Interface Segregation Principle: prefer several small, specific interfaces over one large, general-purpose one, so implementing classes are never forced to provide methods that make no sense for them.

// VIOLATES ISP — RobotWorker is forced to implement eat(), which makes no sense for it

interface Worker {

void work();

void eat();

}

class RobotWorker implements Worker {

public void work() { System.out.println("Working..."); }

public void eat() { throw new UnsupportedOperationException("Robots don't eat"); }

}

// FOLLOWS ISP — split into focused interfaces; implement only what's relevant

interface Workable {

void work();

}

interface Eatable {

void eat();

}

class HumanWorker implements Workable, Eatable {

public void work() { System.out.println("Working..."); }

public void eat() { System.out.println("Eating lunch..."); }

}

class RobotWorker implements Workable {   // no forced, meaningless eat() method

public void work() { System.out.println("Working..."); }

}

Dependency Inversion Principle: high-level code should depend on abstractions (interfaces), not on concrete, low-level implementation classes. This is the principle that makes code genuinely testable and swappable.

// VIOLATES DIP — NotificationService is hard-wired to one specific, concrete sender

class EmailSender {

void send(String message) {

System.out.println("Emailing: " + message);

}

}

class NotificationService {

private EmailSender sender = new EmailSender();   // tightly coupled to EmailSender

void notify(String message) {

sender.send(message);

}

}

// FOLLOWS DIP — NotificationService depends on an abstraction, not a concrete class

interface MessageSender {

void send(String message);

}

class EmailSender implements MessageSender {

public void send(String message) {

System.out.println("Emailing: " + message);

}

}

class SmsSender implements MessageSender {

public void send(String message) {

System.out.println("Texting: " + message);

}

}

class NotificationService {

private final MessageSender sender;

NotificationService(MessageSender sender) {   // the dependency is supplied, not created here

this.sender = sender;

}

void notify(String message) {

sender.send(message);

}

}

NotificationService viaEmail = new NotificationService(new EmailSender());

NotificationService viaSms = new NotificationService(new SmsSender());

Letter

Principle

What It Prevents

S

Single Responsibility

Classes that change for too many unrelated reasons

O

Open/Closed

Editing existing, tested code every time a new variant is added

L

Liskov Substitution

Subclasses that silently break the behaviour callers rely on

I

Interface Segregation

Classes forced to implement methods irrelevant to them

D

Dependency Inversion

High-level code tightly bound to specific, hard-to-swap implementations

Quick Tip

Notice that the Dependency Inversion fix above is what makes the NotificationService class easy to unit test: you can pass in a fake MessageSender during a test, instead of every test triggering a real email or SMS. This is one of the most concrete, practical benefits of following SOLID consistently, not just cleaner code, but code that is genuinely easier to verify.