SOLID Design Principles Explained with Java Examples

Some ideas in software engineering age badly. The SOLID design principles are not among them. Robert C. Martin (Uncle Bob) introduced them around 2000 in his paper “Design Principles and Design Patterns”, and Michael Feathers later packaged them into the famous acronym. These five rules have shaped how engineers design object-oriented systems for more than two decades.

If you care about code that is loosely coupled, highly cohesive, easy to test, and cheap to maintain, SOLID is still the foundation. This guide walks through each principle with short Java examples. The principles themselves are language-agnostic: they apply equally well in C#, TypeScript, Python, or Go.

What Does SOLID Stand For?

SOLID is an acronym for five object-oriented design principles:

  • S: Single Responsibility Principle
  • O: Open/Closed Principle
  • L: Liskov Substitution Principle
  • I: Interface Segregation Principle
  • D: Dependency Inversion Principle

Together they aim at one outcome: software that can change without breaking. Let’s look at each one.

The Five SOLID Principles, One by One

1. Single Responsibility Principle (SRP)

A class should have one responsibility. That gives it exactly one reason to change. The payoff is concrete: classes with a single job are easier to test, carry fewer dependencies, and keep the overall system cohesive and readable.

Consider a simple domain class:

public class Vehicle {
    private String model;
    private String version;
    private Integer year;
    // constructor, getters and setters
}

Now suppose you need to print vehicle details to the console. The tempting shortcut is to add a printVehicleInfo() method directly to Vehicle. That violates SRP, though: formatting output is not the vehicle’s job. Instead, extract it:

public class VehiclePrinter {
    void printVehicleInfoToConsole(String model, String version, Integer year) {
        // formatting and printing logic
    }
}

Vehicle stays focused on its own data; VehiclePrinter owns presentation. Each class now changes for exactly one reason.

2. Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification. The reasoning is risk management: every time you edit tested, working code, you risk introducing regressions. Extending it instead leaves the proven behavior untouched.

If you need to model vehicles that carry cargo, don’t bolt a storageCapacity field onto Vehicle. Extend it:

public class Truck extends Vehicle {
    private Integer storageCapacity;
    // constructor, getters and setters
}

The parent class is never modified, so everything that already depends on it keeps working.

3. Liskov Substitution Principle (LSP)

Arguably the trickiest of the five: subtypes must be substitutable for their base types without breaking the program. If class A is a subtype of B, any code that works with B must work with A.

Here is a classic violation. Given an interface:

public interface Vehicle {
    void turnOnEngine();
    void accelerate();
}

A MotorCar implements it naturally. But an ElectricBike that throws AssertionError("I don't have an engine!") from turnOnEngine() breaks LSP, since callers can no longer treat all Vehicle instances uniformly. The fix is usually to rethink the abstraction: split the interface into more specialized contracts so every implementation can honor the full contract it claims.

4. Interface Segregation Principle (ISP)

Large interfaces should be split into smaller, more specialized ones. No client should be forced to depend on methods it doesn’t use. Fat interfaces destroy cohesion and drag implementations into exactly the LSP violations described above. Prefer several narrow interfaces, and reach for interface inheritance when you need finer granularity.

5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules; both should depend on abstractions. Suppose a bank’s Customer class holds a concrete Account. That works, but it hard-wires the two together. Invert the dependency by programming against an interface:

public interface Account {
    Double balance();
    void withdraw(Double value);
    void deposit(Double value);
}

public class CheckingAccount implements Account { /* ... */ }
public class SavingsAccount implements Account { /* ... */ }

public class Customer {
    private Account account;
    Customer(Account account) {
        this.account = account;
    }
}

Customer now accepts anything that implements Account. Coupling drops, testability rises (mock the interface), and new account types plug in without touching existing code.

Why SOLID Still Matters in 2020s Architecture

SOLID was born in the world of monolithic OO codebases, but its ideas scale up. The Single Responsibility and Dependency Inversion principles are the intellectual ancestors of well-bounded services in a microservices architecture, and interface segregation is exactly what good API contract design looks like between teams. Whether the unit is a class or a deployable service, the goal is the same: components that can evolve independently. Deciding where to draw those boundaries (between a shared library and a shared service, for example) is SOLID thinking applied at system scale.

Frequently Asked Questions

Are SOLID principles only for object-oriented languages?

No. The examples are usually shown in Java or C#, but the underlying ideas of single responsibility, depending on abstractions, and small focused contracts apply just as well to functional codebases, Go packages, and even service boundaries in distributed systems.

Can applying SOLID be overkill?

Yes. SOLID is a set of heuristics, not laws. Extracting an interface for a class that will only ever have one implementation adds indirection without benefit. Apply the principles where change is likely; keep simple things simple.

What’s the best principle to start with?

Single Responsibility. It delivers the fastest wins in testability and readability, and practicing it naturally leads you toward the other four.

Build It Right the First Time

Principles like SOLID are what separate software that lasts from software you rewrite in two years. At Stage28, our AI-native senior engineers apply this discipline to every engagement: fixed-scope, fixed-price projects with a clear specification up front. You only pay after delivery. If you have a system to build or a codebase that needs a serious refactor, talk to our team and get a scoped proposal.

Have a software project with a clear scope? Get a fixed-price proposal and pay only after delivery.

Get my proposal

Este site usa cookies para personalizar conteúdo e analisar o tráfego do site. Conheça a nossa Política de Cookies.