W3docs

Java SOLID Principles

Apply the SOLID principles — SRP, OCP, LSP, ISP, DIP — to Java design.

Java SOLID Principles

SOLID is a set of five object-oriented design principles — popularised by Robert C. Martin — that keep Java code easy to change, test, and extend as it grows. They are not syntax rules the compiler enforces; they are guidelines for where to draw boundaries between classes so that one change does not ripple through the whole codebase. The acronym stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.

The five principles at a glance

Each letter targets a specific kind of design pain. Keep this table nearby while reading the rest of the chapter:

LetterPrincipleOne-line goal
SSingle ResponsibilityA class should have one reason to change
OOpen/ClosedOpen for extension, closed for modification
LLiskov SubstitutionSubtypes must be usable wherever their base type is
IInterface SegregationMany small interfaces beat one fat one
DDependency InversionDepend on abstractions, not concrete classes

The principles reinforce each other. In well-factored code you rarely apply just one — a small interface (ISP) that high-level code depends on (DIP) is exactly what lets you add a new implementation (OCP) without touching the caller.

S — Single Responsibility Principle

A class should do one thing and have one reason to change. When unrelated concerns — business rules and message delivery, say — share a class, a change to either forces you to retest both. Splitting them isolates change.

// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
  void raise(String user, int errors) {
    if (errors > 0) {
      // ...build an email, open an SMTP connection, send...
    }
  }
}

// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
  private final Notifier notifier;
  AlertService(Notifier notifier) { this.notifier = notifier; }
  void raise(String user, int errors) {
    if (errors > 0) notifier.send(user, errors + " error(s) detected");
  }
}

O — Open/Closed Principle

Software entities 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 — code that already works. In Java the usual lever is a stable interface plus new implementations.

interface Notifier { void send(String to, String message); }

class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier   implements Notifier { /* ... */ } // new feature = new class

// AlertService never changes when a new channel appears.

Adding push notifications later means writing PushNotifier implements NotifierAlertService is untouched, so it needs no re-review and no regression risk.

L — Liskov Substitution Principle

If S is a subtype of T, then objects of type T can be replaced with objects of type S without breaking the program. A subclass must honour the contract of its parent — same expectations, no surprising exceptions, no stricter preconditions.

abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle    extends Shape { /* area() = PI * r * r */ }

// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}

The classic violation is Square extends Rectangle: if setting the width also mutates the height, code written for a Rectangle breaks when handed a Square. The fix is to model them as siblings under Shape, not a parent-child pair.

I — Interface Segregation Principle

Clients should not be forced to depend on methods they do not use. Prefer several small, focused interfaces over one large one — otherwise an implementer is dragged into stubbing methods it cannot honour.

// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }

// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }

class ConfigFile implements Readable {        // no empty write() stub
  public String read() { return "mode=prod"; }
}

D — Dependency Inversion Principle

High-level modules should not depend on low-level modules; both should depend on abstractions. In practice: code against interfaces and inject the concrete implementation (constructor injection is the simplest form). This is what makes the other principles pay off — and what makes a class testable, since you can pass in a fake.

// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.

A worked example: all five in one program

This program wires the principles together — a single AlertService (SRP) talks to an injected Notifier (DIP), swaps between an EmailNotifier and an SmsNotifier without changing (OCP), reads a Readable-only ConfigFile (ISP), and sums areas across Shape subtypes uniformly (LSP). It checks its own results so you can see each principle hold.

java— editable, runs on the server

What to take from the run:

  • email sent: [EMAIL -> alice: 3 error(s) detected] contains only one entry — bob had zero errors so raise sent nothing. AlertService owns the single responsibility of deciding when to alert (SRP); it never builds a message body or opens a connection.
  • The same AlertService class drove both an EmailNotifier and an SmsNotifier because it was handed the dependency through its constructor (DIP). The high-level alerting logic depends only on the Notifier interface, never on a concrete sender.
  • OCP check : ... unchanged = true confirms both alert objects are the same AlertService class: adding SMS support meant writing a new SmsNotifier, with zero edits to AlertService — open for extension, closed for modification.
  • ISP check : is Writable? false shows ConfigFile implements Readable only. Because the interfaces are segregated, the read-only source was never forced to provide a meaningless write stub.
  • LSP area : 9.142 is the sum of a 2×3 rectangle (6.0) and a radius-1 circle (≈3.142). totalArea looped over Shape references and called area() without checking which subtype it held — every subtype was substitutable for its base (LSP).

Practice

Practice

A class named ReportGenerator both formats report data and writes it to disk, so any change to either the formatting rules or the file layout forces you to modify and retest the same class. Which SOLID principle does this most directly violate?