A Practical, Code‑Dense Tour of the SOLID Principles in Java

By someone who has spent too many Friday nights undoing “harmless” feature requests.


Table of Contents

  1. Introduction: Why SOLID Still Matters
  2. S — Single‑Responsibility Principle
  3. O — Open/Closed Principle
  4. L — Liskov Substitution Principle
  5. I — Interface Segregation Principle
  6. D — Dependency Inversion Principle
  7. SOLID Sanity Checklist
  8. Final Thoughts: Future‑You Deserves Better

Introduction: Why SOLID Still Matters

“But we’re microservices now, SOLID is for 2000‑era JavaBeans!”

Cute. The truth is: each microservice can still rot into a tiny monolith—hard to test, risky to change, impossible to extend. SOLID is your mold‑resistant paint.

Below is a full‑fat walkthrough. Every code snippet is production‑ish and comes with its matching anti‑pattern so you can recognise the smell before your code review does.


S — Single‑Responsibility Principle

“A class should have one, and only one, reason to change.”

🚫 How We Break It

class InvoiceService {                   // three jobs jammed together
    Money calculateTotal(List<Item> items) { /* pricing rules */ }
    void   printPdf(Invoice inv)        { /* rendering */ }
    void   email(Invoice inv, String to){ /* SMTP stuff */ }
}

✅ How We Fix It

class InvoiceCalculator { /* math only */ }
class InvoicePdfRenderer { /* produce PDF bytes */ }
class InvoiceMailer      { /* fire off the email */ }

Now pricing tweaks don’t recompile the PDF engine, and SMTP outages stop spamming the accounting team’s pull requests.


O — Open/Closed Principle

“Open for extension, closed for modification.”

If adding a feature means editing old code, you’re doing it wrong.

3.1 Strategy vs. Giant switch

🚫 One‑Way Ticket to switch Hell

double discount(Cart cart) {
    switch (cart.type()) {
        case REGULAR: return 0;
        case MEMBER:  return cart.total() * 0.05;
        case VIP:     return cart.total() * 0.10;
        default: throw new IllegalStateException();
    }
}

✅ Strategy Pattern

interface DiscountPolicy { double apply(Cart cart); }

class NoDiscount     implements DiscountPolicy { public double apply(Cart c){return 0;} }
class MemberDiscount implements DiscountPolicy { public double apply(Cart c){return c.total()*0.05;} }
class VipDiscount    implements DiscountPolicy { public double apply(Cart c){return c.total()*0.10;} }

A new policy (BlackFridayDiscount) is a new class. Core code sleeps peacefully.

3.2 Plug‑in Files with ServiceLoader

Scenario: Exporting data to “whatever format product dreams up next”.

public interface Exporter {
    String format();               // "csv", "json", "parquet", ...
    void   write(Data d, Path p);
}
Loader
ServiceLoader<Exporter> loader = ServiceLoader.load(Exporter.class);

Map<String, Exporter> exporters =
    StreamSupport.stream(loader.spliterator(), false)
                 .collect(Collectors.toMap(Exporter::format, e -> e));

exporters.get(requestedFormat).write(data, path);

Publish a JAR containing ParquetExporter, list it in

META-INF/services/com.acme.Exporter, bounce the JVM—zero edits.

3.3 Taxes Without Tears (Chain of Responsibility)

interface TaxRule {
    boolean applies(Bill b);
    double  apply(Bill b);
}
class IndiaVat         implements TaxRule { ... }
class LuxurySurcharge  implements TaxRule { ... }

class TaxCalculator {
    private final List<TaxRule> rules;
    TaxCalculator(List<TaxRule> rules){ this.rules = rules; }
    double compute(Bill b){
        return rules.stream()
                    .filter(r -> r.applies(b))
                    .mapToDouble(r -> r.apply(b))
                    .sum();
    }
}

Next fiscal loophole = one class in the list. Finance sleeps, you sleep.

3.4 Decorators for Logging, Encryption & Friends

interface DataStore { void save(byte[] bytes); }

class DiskStore implements DataStore { ... }

class EncryptingStore implements DataStore {
    private final DataStore inner;
    EncryptingStore(DataStore i){ this.inner = i; }
    public void save(byte[] b){ inner.save(Aes.encrypt(b)); }
}

class CompressingStore implements DataStore { ... }

// Compose as needed:
DataStore store = new CompressingStore(
                     new EncryptingStore(new DiskStore()));

Add caching, metrics, throttling—keep stacking wrappers.

3.5 Feature Flags with Functional Interfaces

@FunctionalInterface
interface Greeting { String msg(String name); }

@Component
class Greeter {
    private final Greeting g;
    Greeter(Greeting g){ this.g = g; }
    String greet(String n){ return g.msg(n); }
}

// default bean
@Bean Greeting defaultGreeting() {
    return n -> "Hello " + n;
}

// Christmas bean (activated via profile/flag)
@Bean @Profile("xmas") Greeting xmasGreeting() {
    return n -> "🎄 Merry Christmas, " + n + "!";
}

Flip the profile → new behaviour. No code edits.


L — Liskov Substitution Principle

“Subtypes must honour the parent’s contract—no hidden disclaimers.”

🚫 Broken Promise

class FileReader {
    String read(Path p) { /* … */ }
}

class NetworkFileReader extends FileReader {
    @Override
    String read(Path p) { throw new UnsupportedOperationException("remote only"); }
}

Works until someone innocently passes NetworkFileReader where FileReader was expected.

✅ Honest Abstraction

interface Reader { String read() throws IOException; }

class LocalFileReader implements Reader { ... }
class RemoteUrlReader implements Reader { ... }

Both obey the same rules; clients remain blissfully ignorant.


I — Interface Segregation Principle

“Many small, purpose‑built interfaces beat one kitchen‑sink.”

🚫 One Ring to Bore Them All

interface DataStore {
    void save(Object o);
    Object fetch(String id);
    void flush();
    void compact();
    MigrationReport migrate();   // only admin tools use this
}

✅ Slice It Up

interface Saver   { void save(Object o); }
interface Fetcher { Object fetch(String id); }

interface AdminOps extends Saver, Fetcher {
    void flush();
    void compact();
    MigrationReport migrate();
}

Everyday services depend only on Saver + Fetcher, not on arcane admin lore.


D — Dependency Inversion Principle

“High‑level policy must not depend on low‑level plumbing.”

🚫 Hard‑Wired DAO

class OrderService {
    private final JdbcOrderDao dao = new JdbcOrderDao();  // welded‑in

    void place(Order o) { dao.persist(o); }
}

✅ Inject an Abstraction

interface OrderRepository { void save(Order o); }

class JdbcOrderRepository  implements OrderRepository { ... }
class NoSqlOrderRepository implements OrderRepository { ... }

class OrderService {
    private final OrderRepository repo;        // constructor injection
    OrderService(OrderRepository repo){ this.repo = repo; }

    void place(Order o){ repo.save(o); }
}

Unit tests swap in an in‑memory stub; prod swaps in a shiny, replicated DB driver.


SOLID Sanity Checklist

Principle Ask Yourself… Quick Smell
SRP “Will this class change for more than one reason?” 200‑line class with 6 verbs in its name
OCP “Can I add a feature without editing core logic?” God‑switch blocks / if pyramids
LSP “Could a subclass break the parent’s guarantees?” UnsupportedOperationException in overrides
ISP “Are my consumers forced to implement stuff they never use?” Interfaces ending in “Manager” or “Service” with 10+ methods
DIP “Am I new‑ing concrete classes deep inside business code?” Tests that need a real database just to compile

Print it, tape it next to your monitor, thank me later.


Final Thoughts: Future‑You Deserves Better

SOLID isn’t ivory‑tower dogma; it’s a survival kit.

  • SRP stops the avalanche rebuild.
  • OCP lets you ship Friday features without Saturday hotfixes.
  • LSP kills polymorphic land‑mines.
  • ISP keeps your mocks tiny and your APIs honest.
  • DIP makes swapping databases or message buses a wiring exercise, not a rewrite.

Write code like you’ll be on‑call at 2 a.m.—because spoiler: you will be.

Keep it SOLID, and those 2 a.m. pages might just stay someone else’s problem.