Sitemap
Minoli Alles

Minoli Alles

🚀 Mastering SOLID Principles in Java — Made Simple

7 min readApr 22, 2025

Writing clean and maintainable code is like building a strong foundation for a skyscraper. If your base is shaky, everything else is at risk. That’s where SOLID principles come in — a set of five golden rules for writing better object-oriented code.

Let’s break each principle down with simple examples in Java so you’ll not only understand them but also know how to apply them in your next project.

📌 1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change — meaning it should only do one thing

Imagine you’re building a simple employee management system. You create a class to store employee details and to save them to a file.

🚫 Violation of SRP

public class Employee {
private String name;
private String position;

public Employee(String name, String position) {
this.name = name;
this.position = position;
}

public String getDetails() {
return name + " - " + position;
}

public void saveToFile(String filename) {
// logic to save employee details to a file
}
}

Here, the Employee class is handling two responsibilities:

  1. Representing employee data.
  2. Saving data to a file.

So, it has two reasons to change:

  • If the data structure changes.
  • If the file-saving method changes.

This violates SRP.

We should separate the responsibilities into two classes.

✅ Following SRP

Create a new class for representing employee data:

public class Employee {
private String name;
private String position;

public Employee(String name, String position) {
this.name = name;
this.position = position;
}

public String getDetails() {
return name + " - " + position;
}
}

And create another class for saving to a file:

public class EmployeeFileSaver {
public void saveToFile(Employee employee, String filename) {
// logic to save employee.getDetails() to a file
}
}

Now:

  • Employee only knows about employee data.
  • EmployeeFileSaver is responsible for file I/O.

Each class has one job, and one reason to change. ✅

🌟 Why SRP Matters

  • 🧪 Easier to test — You can test file saving without touching employee data.
  • 🔄 Flexible — You can change how files are saved (e.g., to a database) without touching your employee class.
  • 📚 Clear structure — Code is easier to read, maintain, and extend.

📌 2. Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.

In other words: You should be able to add new features without changing existing code.

🚫 Violation of OCP

Let’s say you have a basic notification system that supports only email.

public class NotificationService {
public void send(String message, String type) {
if (type.equals("EMAIL")) {
// send email
}
}
}

Now your team asks for SMS notifications. You go in and add:

if (type.equals("SMS")) {
// send SMS
}

What happens next month? You’re adding WhatsApp, then Push, then… 😵
This class is getting modified every time — violating OCP.

✅ Following OCP

Let’s refactor using abstraction.

public interface Notifier {
void send(String message);
}

Now we implement different notification types:

public class EmailNotifier implements Notifier {
public void send(String message) {
// send email
}
}

public class SmsNotifier implements Notifier {
public void send(String message) {
// send SMS
}
}

Then a service class can use any notifier:

public class NotificationService {
private Notifier notifier;

public NotificationService(Notifier notifier) {
this.notifier = notifier;
}

public void notify(String message) {
notifier.send(message);
}
}

Now, to add a new channel (e.g., WhatsApp), you simply:

  • Create a new class that implements Notifier.
  • No need to touch existing code. ✅

🌟 Why OCP Matters

  • 🚫 Avoids bugs in working code by not modifying it.
  • 🔧 Easy to extend — new functionality doesn’t disturb the old.

📌 3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without altering the behavior of the program.

In simpler words: If class B is a subclass of class A, you should be able to use B wherever you use A — and everything should still work as expected.

🚫 Violation of LSP

Let’s say we have a base class for a Bird:

public class Bird {
public void fly() {
System.out.println("Flying high!");
}
}

Now we extend it:

public class Sparrow extends Bird {
// inherits fly() - all good
}

public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly!");
}
}

If somewhere in your code you write:

public void letBirdFly(Bird bird) {
bird.fly(); // Uh oh! Ostrich will crash this.
}

This violates LSP — Ostrich is a Bird, but it can’t fly like the rest. Substituting it causes trouble.

Get Minoli Alles’s stories in your inbox

Join Medium for free to get updates from this writer.

✅ Following LSP

We fix this by being more accurate with our inheritance. Let’s separate flying behavior using interfaces.

public interface Flyable {
void fly();
}

Now:

public class Sparrow implements Flyable {
public void fly() {
System.out.println("Sparrow flying!");
}
}

public class Ostrich {
// Ostrich does not implement Flyable
}

And in your code:

public void letItFly(Flyable bird) {
bird.fly(); // Now only flyable birds are allowed
}

No surprises, no crashes. ✅

🌟 Why LSP Matters

  • 🛠️ Prevents runtime errors from incorrect assumptions.
  • 🔄 Keeps code behavior predictable when using subclasses.
  • 🚦 Encourages accurate modeling of relationships between classes.

📌 4. Interface Segregation Principle (ISP)

Definition: A class should not be forced to implement interfaces it does not use.

In simpler terms: Don’t dump a bunch of unrelated responsibilities into one interface — instead, split them into smaller, more specific ones.

🧶Real-World Analogy — Think of a restaurant menu.

If you’re vegetarian, you should only see the Veg Menu, not a combined menu with seafood, steak, and sushi.
Being forced to go through irrelevant items wastes your time — and in code, it clutters your design.

🚫 Violation of ISP

Let’s say we have a big interface for a multifunction printer:

public interface MultiFunctionDevice {
void print();
void scan();
void fax();
}

Now you create a simple BasicPrinter:

public class BasicPrinter implements MultiFunctionDevice {
public void print() {
System.out.println("Printing...");
}

public void scan() {
// Not supported
throw new UnsupportedOperationException("Scan not supported");
}

public void fax() {
// Not supported
throw new UnsupportedOperationException("Fax not supported");
}
}

You see the problem? BasicPrinter is forced to implement methods it doesn’t need. That’s a violation of ISP.

✅ Following ISP

Let’s split the big interface into smaller, specific ones:

public interface Printer {
void print();
}

public interface Scanner {
void scan();
}

public interface FaxMachine {
void fax();
}

Now, BasicPrinter only implements what it needs:

public class BasicPrinter implements Printer {
public void print() {
System.out.println("Just printing, that’s it!");
}
}

And a more advanced device can implement multiple interfaces:

public class OfficePrinter implements Printer, Scanner, FaxMachine {
public void print() {
System.out.println("Printing document...");
}

public void scan() {
System.out.println("Scanning document...");
}

public void fax() {
System.out.println("Faxing document...");
}
}

Much cleaner! ✅

🌟 Why ISP Matters

  • 🧼 Cleaner code — no more “dummy” methods with exceptions.
  • 🧩 Flexible — small interfaces let you mix and match only what you need.
  • 🧘 Less stress — developers don’t have to worry about unnecessary stuff.

📌 5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules.
Both should depend on abstractions.

Also: Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

🧶Real-World Analogy — Think about a universal charger.

It doesn’t care whether you’re plugging in a phone, camera, or tablet — it just expects the device to follow a common standard (like USB-C).
The charger isn’t tightly tied to specific gadgets — it’s decoupled.

🚫 Violation of DIP

Here’s a tightly coupled example:

public class Keyboard {
public String getInput() {
return "Key Pressed";
}
}

public class Computer {
private Keyboard keyboard = new Keyboard();

public void bootUp() {
System.out.println("Booting... " + keyboard.getInput());
}
}

The Computer directly depends on the Keyboard class.
If you ever wanted to use a different input device (like a VirtualKeyboard), you’d have to change the Computer class — not ideal.

✅ Following DIP with Abstraction

Let’s introduce an interface:

public interface InputDevice {
String getInput();
}

Now update both devices:

public class Keyboard implements InputDevice {
public String getInput() {
return "Keyboard input";
}
}

public class VirtualKeyboard implements InputDevice {
public String getInput() {
return "Virtual input";
}
}

And update the Computer to depend on the abstraction:

public class Computer {
private InputDevice inputDevice;

public Computer(InputDevice inputDevice) {
this.inputDevice = inputDevice;
}

public void bootUp() {
System.out.println("Booting... " + inputDevice.getInput());
}
}

Now you can inject any input device without changing the Computer class.

public class Main {
public static void main(String[] args) {
InputDevice keyboard = new Keyboard();
Computer pc = new Computer(keyboard);
pc.bootUp();
}
}

Want to switch to a virtual keyboard? Just inject it — no need to touch the core logic! ✅

🌟 Why DIP Matters

  • 🔌 Flexible architecture — swap implementations easily.
  • 🚀 Testable code — you can mock dependencies for unit testing.
  • 🧱 Decoupled design — improves maintainability and scalability.

Wrapping Up

Mastering the SOLID principles isn’t just about memorizing five rules — it’s about embracing a mindset that leads to cleaner, more maintainable, and scalable code. Whether you’re building small features or architecting enterprise systems, these principles serve as your compass in the chaotic world of software development.

Start small. Refactor often. Think in abstractions.
And always remember — writing good code is an art, but writing SOLID code is a discipline.

🚀 Keep learning, keep coding, and keep it SOLID!

We should separate the responsibilities into two classes.

--

--