The idea: SOLID is about the cost of a change

The five SOLID principles, collected by Robert C. Martin, are rules for arranging classes and the dependencies between them. They don't change what a program does. They change what it costs to change it: how many classes a request touches, how many must be rebuilt and retested, and whether something nobody meant to touch breaks.

So each tab shows the same small program designed two ways: ✗ violates on the left, ✓ follows on the right. A Call runs a request through both; it gives the same answer. A Change sends both the same change request, and the scoreboards under them show the difference.

How to read the page

  • A box is a class: its name on top, then its fields and methods. A blue box is an «interface». An arrow A → B means "the source code of A mentions B": A uses it, creates it with new, or implements / extends it (blue).
  • A change request flies in from the person who asked for it. The classes a developer must edit turn orange (the edited line gets a ✎), new classes turn teal.
  • Then the ripple: whatever depends on a changed class must be recompiled, retested and redeployed (yellow). It spreads backwards along the arrows, one step at a time. An interface that did not change stops it: code that only knows the interface is not affected by a new or edited implementation.
  • Every test that exercises a rebuilt class runs again. Its result is computed (the pay formula, the discount table, …), so a regression shows up as a red ✗ with what it got and what it wanted.

S: Single Responsibility

"A class should have one reason to change." Martin's sharper version: a module should be responsible to one, and only one, actor, the group of people who ask for changes to it. The Employee class has three: the CFO's accounting (calculatePay), the COO's HR (reportHours) and the CTO's DBAs (save).

Both pay and the hours report call one private helper, regularHours(). When the CFO asks for overtime to start after 42 hours, the developer edits that helper. Pay becomes $930, as asked. But the COO's legal hours report also says 42 regular hours now. Nobody asked for that, and only HoursReportTest catches it. On the right each actor has its own class with its own copy of regularHours(). The copies look like duplication, but they are not: they answer to different people and must be free to change apart. (Demo: overtime change)

Even a harmless change costs more on the left: the CTO's new table edits Employee, so pay and hours are rebuilt, retested and redeployed too (Demo: new DB table).

Left, violates: the CFO, COO and CTO all depend on one Employee class; calculatePay and reportHours both call the shared regularHours, so the CFO's change to 42 hours also changes the hours report and HoursReportTest fails. Right, follows: CFO uses PayCalculator, COO uses HourReporter, CTO uses EmployeeSaver, each reading EmployeeData; only PayCalculator's own regularHours changes
Split by actor: each person's change request then edits only the class that answers to them.

O: Open/Closed

"Software entities should be open for extension, but closed for modification" (Bertrand Meyer, 1988). Adding a feature should mean adding code, not editing code that already works and is already tested.

The left side picks the discount with a switch (type) in CheckoutService, and the invoice label with another switch in InvoicePrinter. Adding VIP customers means editing the enum and every switch on it. The developer finds one and misses the other: the price is right, the invoice is not. On the right each type is a DiscountPolicy class that knows its rate and its label. VIP is a new class, VipPolicy, plus one wiring line in Main, where types are mapped to policies. Checkout and invoice code is not opened. (Demo: add VIP)

"Closed" is never total: something has to choose which class to create. The trick is to push that choice into one place, a factory or the program's start-up code, instead of every place that needs the behaviour.

L: Liskov Substitution

Barbara Liskov (1987), later with Jeannette Wing: if S is a subtype of T, then code written for a T must keep working when given an S. A subtype may accept more and promise more, never less. In contract terms: preconditions no stronger, postconditions no weaker, and the parent's invariants kept.

Square extends Rectangle looks right ("a square is a rectangle") and compiles. But a mutable Rectangle promises that setHeight leaves the width alone. A square can't keep that promise and stay square, so its setters change both sides. stretch() sets width 5 and height 4 and gets an area of 16, not 20, at run time, in code that never heard of squares. On the right both are immutable Shapes. Code that needs an area takes a Shape. Code that needs independent sides takes a Rectangle, and the compiler rejects a square there. (Demo: Square as Rectangle)

"Is-a" in code is about behaviour, not about how things are named in the real world.

I: Interface Segregation

"No client should be forced to depend on methods it does not use." One fat Machine interface with print, scan and fax ties every job and every machine to all three methods. When fax() gets a phone-number parameter, SimplePrinter has to be edited too, though all it can do with a fax is throw. And all six classes are rebuilt. With Printer, Scanner and Fax interfaces, only the three classes that fax are touched. (Demo: change fax())

A method whose only body is throw new UnsupportedOperationException() is a sign: the interface promises something the class can't do. Users find it at run time. With small interfaces the compiler finds it first (Demo: scan on a printer). This is also a Liskov problem: that printer is not a substitutable Machine.

D: Dependency Inversion

"High-level modules should not depend on low-level modules; both should depend on abstractions." The high-level policy (OrderService: what placing an order means) should not name the low-level detail (MySqlOrderRepo: how rows reach MySQL).

On the left the service does new MySqlOrderRepo(), so its source arrow points down into the details. On the right the service depends on an OrderRepository interface. The interface sits in the policy band, because the service owns it and defines it in its own terms. MySqlOrderRepo implements it, so its arrow points up. That is the inversion: at run time control still flows down (controller → service → repository → database), but the source dependency points against it (Call: placeOrder).

What it buys: OrderServiceTest injects an in-memory fake and runs with no database (Demo: unit test). Moving to PostgreSQL is a new class plus one line in Main, the composition root where concrete classes are chosen; the business logic is not opened (Demo: move to PostgreSQL). Note that dependency injection (passing the repository in) is the usual mechanism, but the principle is about which way the arrow points and who owns the interface.

Applied to a whole application, this gives ports and adapters, also called hexagonal architecture: see Hexagonal Architecture.

Left, violates: OrderController uses OrderService, which does new MySqlOrderRepo, so its arrow points down from the policy band into the MySQL details band. Right, follows: OrderService uses the OrderRepository interface in the policy band, and MySqlOrderRepo in the details band implements it, so its arrow points up
Dependency inversion: the policy owns the interface, so the source arrow from the MySQL detail points up into the policy.

The five in one table

PrincipleSmellFixWhat the scoreboard shows
S: single responsibilityone class, several actors; shared helpers between their featuressplit by actorovertime: rebuilt 4 vs 2, failed 1 vs 0
O: open/closedswitch / if on a type code, repeatedpolymorphism, one class per caseadd VIP: 2 classes edited + 1 missed vs 1 wiring line
L: Liskov substitutionan override that weakens a promise, or throwsdon't inherit; share a smaller interfacerun-time failure vs compile error
I: interface segregationfat interface; stub methods that throwone small interface per client rolechange fax(): rebuilt 6 vs 3
D: dependency inversionnew ConcreteDetail() inside business logican interface owned by the policy; inject the detailunit test fails vs passes; PostgreSQL: rebuilt 3 vs 2

When not to apply them

Each principle adds a seam: another class, another interface, another indirection to read through. A seam pays off where change actually happens, and costs where it doesn't. An interface for every class, a strategy for a switch that will never grow, or a plugin point "in case" makes code harder to follow, not easier. A practical rule: write the simple version, and add the seam the second time that kind of change comes in, or as soon as a test needs it.

What the page leaves out

  • Real builds are smarter: Java tools recompile only on API (signature) changes, and binary compatibility lets some changes skip dependents. The page uses the simple rule "a change ripples to every dependent", which is what redeploy and retest usually look like.
  • Martin's component principles (REP, CCP, CRP, ADP, SDP, SAP), the same ideas at the level of packages and services.
  • Functional-style equivalents: passing functions instead of strategy objects, modules instead of classes.
  • Mocks, test doubles and DI containers beyond a hand-written fake and a Main.

See also Java compilation for what "rebuild" means for a class, and Hexagonal Architecture for dependency inversion across a whole application.