DEV Community

Dhanush B
Dhanush B

Posted on

Mastering SOLID Principles in Java: Real-World Examples and Best Practices for Scalable Code

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 */ }
}
Enter fullscreen mode Exit fullscreen mode

✅ How We Fix It

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

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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;} }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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()));
Enter fullscreen mode Exit fullscreen mode

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 + "!";
}
Enter fullscreen mode Exit fullscreen mode

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"); }
}
Enter fullscreen mode Exit fullscreen mode

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 { ... }
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

✅ 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();
}
Enter fullscreen mode Exit fullscreen mode

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); }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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); }
}
Enter fullscreen mode Exit fullscreen mode

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.

MongoDB Atlas runs apps anywhere. Try it now.

MongoDB Atlas runs apps anywhere. Try it now.

MongoDB Atlas lets you build and run modern apps anywhere—across AWS, Azure, and Google Cloud. With availability in 115+ regions, deploy near users, meet compliance, and scale confidently worldwide.

Start Free

Top comments (0)

DevCycle image

Ship Faster, Stay Flexible.

DevCycle is the first feature flag platform with OpenFeature built-in to every open source SDK, designed to help developers ship faster while avoiding vendor-lock in.

Start shipping

👋 Kindness is contagious

Dive into this thoughtful piece, beloved in the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A sincere "thank you" can brighten someone's day—leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay