SOLID Principles in Java

Naveen AnamaneniUpdated · 6 min read

Code that works today must also be easy to change tomorrow, when the business adds a new discount, a new payment method or a new way to notify customers. SOLID is a set of five principles, collected by Robert C. Martin, that keep object-oriented code flexible. Each one names a common way designs go wrong, and the fix. This lesson explains all five, then applies them to the OrderManager "god class" from Appendix G2.

The Five Principles

SOLID at a glance
LetterPrincipleIn one lineSmell it fixes
SSingle responsibilityA class should have one reason to changeA class that saves, emails and prints
OOpen-closedOpen for extension, closed for modificationAn if-else chain edited for every new case
LLiskov substitutionA subclass must be usable anywhere its parent isOverrides that throw "not supported" or change the meaning
IInterface segregationClients shouldn't depend on methods they don't useOne fat interface that every class half-implements
DDependency inversionDepend on abstractions, not concrete classesnew MySqlDatabase() inside business logic

Each Principle in Practice

S: single responsibility. Appendix G2's OrderManager validates items, calculates discounts, writes SQL, sends email and builds invoices. A change to the email text, the database or the tax rules all touch the same class. Split it: OrderService, OrderRepository, Notifier, InvoicePrinter, each with one job.

O: open-closed. Discounts there are an if-else on "GOLD", "SILVER", "STAFF". Adding "PLATINUM" means editing tested code. Put each discount behind a DiscountPolicy interface; a new discount is a new class, and nothing old changes (the Strategy pattern).

L: Liskov substitution. The classic broken example is Square extends Rectangle. Code that sets a rectangle's width to 5 and height to 4 expects an area of 20; a Square that keeps its sides equal gives 16. If a subclass can't honor the parent's promises, it shouldn't extend it.

I: interface segregation. A Machine interface with print(), scan() and fax() forces a simple printer to "implement" scanning by throwing an exception. Split it into Printer, Scanner and Fax; a multifunction device implements all three.

D: dependency inversion. OrderManager calls DriverManager.getConnection(...) directly, so it can't run without MySQL, even in a test. Make it depend on an OrderRepository interface, and pass a real or in-memory implementation in through the constructor.

The God Class, Refactored

Here's the heart of the G2 refactor, with a fake repository and notifier so it runs anywhere:

Solid.java▶ Try it
import java.util.ArrayList;
import java.util.List;
 
public class Solid {
    record Item(String name, long price, int qty) {
        long total() { return price * qty; }
    }
 
    interface DiscountPolicy { long apply(long amount); }
    interface OrderRepository { void save(String email, long total); }
    interface Notifier { void send(String email, String text); }
 
    static class GoldDiscount implements DiscountPolicy {
        public long apply(long amount) { return amount * 80 / 100; }
    }
 
    static class NoDiscount implements DiscountPolicy {
        public long apply(long amount) { return amount; }
    }
 
    static class InMemoryRepository implements OrderRepository {
        final List<String> rows = new ArrayList<>();
        public void save(String email, long total) {
            rows.add(email + "=" + total);
        }
    }
 
    static class ConsoleNotifier implements Notifier {
        public void send(String email, String text) {
            System.out.println("To " + email + ": " + text);
        }
    }
 
    static class OrderService {
        private final OrderRepository repository;
        private final Notifier notifier;
 
        OrderService(OrderRepository repository, Notifier notifier) {
            this.repository = repository;
            this.notifier = notifier;
        }
 
        long place(String email, List<Item> items, DiscountPolicy policy) {
            long subtotal = 0;
            for (Item i : items) {
                subtotal += i.total();
            }
            long total = policy.apply(subtotal);
            repository.save(email, total);
            notifier.send(email, "your order total is Rs. " + total);
            return total;
        }
    }
 
    public static void main(String[] args) {
        InMemoryRepository repo = new InMemoryRepository();
        OrderService service = new OrderService(repo, new ConsoleNotifier());
        List<Item> cart = List.of(
            new Item("Headphones", 2500, 1), new Item("Cable", 300, 2));
        service.place("navika@example.com", cart, new GoldDiscount());
        service.place("srihitha@example.com", cart, new NoDiscount());
        System.out.println("Saved: " + repo.rows);
    }
}
Output
To navika@example.com: your order total is Rs. 2480
To srihitha@example.com: your order total is Rs. 3100
Saved: [navika@example.com=2480, srihitha@example.com=3100]

How it works

  1. S: OrderService only coordinates placing an order. Saving, notifying and discounting each live in their own class.
  2. O: a new discount is a new DiscountPolicy class; OrderService never changes.
  3. D: OrderService depends on the OrderRepository and Notifier interfaces, received through its constructor. main plugs in in-memory versions; production code would plug in a JDBC repository and an email sender.
  4. I: each interface has a single method; no class implements anything it doesn't need.
  5. Money is a long of rupees and the item is a record, which removes G2's String[] and double problems too.

Common Mistakes

Splitting classes into tiny pieces with no real reason. One-line classes everywhere are as hard to follow as a god class.

Subclassing for code reuse when the "is-a" promise doesn't hold. That's the Liskov trap; use composition.

Creating dependencies with new inside business code. It welds the class to one implementation.

Interview Corner

What are the SOLID principles?

Single responsibility: a class should have one reason to change. Open-closed: classes should be open for extension but closed for modification, so new behavior comes from new code. Liskov substitution: subtypes must be usable wherever their base type is expected without breaking correctness. Interface segregation: prefer small, client-specific interfaces over large general ones. Dependency inversion: high-level modules should depend on abstractions, not on low-level concrete classes.

Give an example of violating the Liskov substitution principle.

Square extends Rectangle: if a square overrides setWidth to also change the height, code written for rectangles that sets width 5 and height 4 expects an area of 20 but gets 16. The subclass can't be substituted for its parent without surprising callers, so it shouldn't extend it.

Practice

  1. Easy: Add a StaffDiscount (50% off) to Solid.java and place an order with it. Which existing classes did you edit?
  2. Harder: Complete the G2 refactor: add an InvoicePrinter class that prints each item and the total, and call it from OrderService through an interface.

Quick check: SOLID

4 questions
1A class saves orders, sends emails and prints invoices. Which principle does it break?
2Adding a discount means editing an if-else chain. Which principle is broken?
3Which principle does Square extends Rectangle usually break?
4OrderService receives an OrderRepository interface through its constructor. Which principle?

Key Takeaways

Was this tutorial helpful?

Lesson 177 of 482