Compile Ready
All low level design problems
Low Level Design/Concepts & Foundations/ Concept

SOLID Principles

The five principles (SRP, OCP, LSP, ISP, DIP) that separate a maintainable design from a brittle one.

5m readSOLIDSRPOCPLSPISP

Introduction

SOLID is a practical checklist for keeping LLD object models change-friendly. It helps you decide where responsibilities live, which classes may vary independently, and where to introduce interfaces before the design hardens around concrete details.

Use the principles as interview language: first model the domain, then explain how each boundary avoids a known smell. A strong answer does not recite acronyms; it shows why a new payment method, pricing policy, notification channel, or repository can be added with a small additive change.

Learning Objectives

  • Define SRP, OCP, LSP, ISP, and DIP in crisp interview language.

  • Recognize the Java smell each principle prevents before it becomes a design flaw.

  • Refactor violations with Strategy, focused interfaces, substitutable types, and dependency injection.

  • Use SOLID to justify real LLD decisions in systems such as Parking Lot, payment flows, and notification services.

Core Theory

How SOLID guides an LLD interview

Start with requirements and flows, then apply SOLID as a pressure test on the class model.

  • SRP: Which responsibility changes for a different business reason?
  • OCP: Which policy is likely to gain new variants?
  • LSP: Can every subtype safely replace the base type?
  • ISP: Is any client forced to depend on methods it never calls?
  • DIP: Does high-level workflow depend on abstractions instead of infrastructure details?

In practice, SOLID drives concrete LLD choices: Strategy for pricing or routing policies, small role interfaces for capabilities, repositories behind persistence ports, and constructor injection for services that need external collaborators.

SRP - Single Responsibility Principle

Definition: A class should have one primary reason to change.

Violation: One service validates an invoice, saves it, and sends the receipt.

Fix: Keep the domain rule, persistence, and notification as separate collaborators.

Smell prevented: God object, shotgun surgery, and hidden coupling between business rules and infrastructure.

Interviewer probe: They ask for a new receipt channel or a new storage system and watch whether you edit the invoice workflow or swap a collaborator.

class InvoiceService {
    void closeInvoice(Invoice invoice) {
        if (invoice.total() <= 0) {
            throw new IllegalArgumentException("Invalid invoice");
        }
        Database.save(invoice);
        EmailClient.send(invoice.customerEmail(), "Invoice closed");
    }
}

class InvoiceValidator {
    void validate(Invoice invoice) {
        if (invoice.total() <= 0) {
            throw new IllegalArgumentException("Invalid invoice");
        }
    }
}

interface InvoiceRepository {
    void save(Invoice invoice);
}

interface ReceiptSender {
    void send(Invoice invoice);
}

class InvoiceServiceFixed {
    private final InvoiceValidator validator;
    private final InvoiceRepository repository;
    private final ReceiptSender sender;

    InvoiceServiceFixed(InvoiceValidator validator, InvoiceRepository repository, ReceiptSender sender) {
        this.validator = validator;
        this.repository = repository;
        this.sender = sender;
    }

    void closeInvoice(Invoice invoice) {
        validator.validate(invoice);
        repository.save(invoice);
        sender.send(invoice);
    }
}

OCP - Open Closed Principle

Definition: A module should be open for extension but closed for modification.

Violation: The shipping service switches on every shipping method.

Fix: Put the varying policy behind a Strategy interface and register implementations.

Smell prevented: Switch explosion, regression-prone edits, and tests that must be rewritten for every new variant.

Interviewer probe: They ask you to add same-day delivery, surge pricing, or coupon rules. A strong design adds a class instead of editing the central workflow.

class ShippingCostService {
    int cost(String method, Order order) {
        if (method.equals("STANDARD")) {
            return 50;
        }
        if (method.equals("EXPRESS")) {
            return 120;
        }
        throw new IllegalArgumentException("Unknown method");
    }
}

interface ShippingCostStrategy {
    int cost(Order order);
}

class StandardShipping implements ShippingCostStrategy {
    public int cost(Order order) {
        return 50;
    }
}

class ExpressShipping implements ShippingCostStrategy {
    public int cost(Order order) {
        return 120;
    }
}

class ShippingCostServiceFixed {
    private final Map<String, ShippingCostStrategy> strategies;

    ShippingCostServiceFixed(Map<String, ShippingCostStrategy> strategies) {
        this.strategies = strategies;
    }

    int cost(String method, Order order) {
        ShippingCostStrategy strategy = strategies.get(method);
        if (strategy == null) {
            throw new IllegalArgumentException("Unknown method");
        }
        return strategy.cost(order);
    }
}

LSP - Liskov Substitution Principle

Definition: A subtype must be usable wherever its base type is expected without breaking correctness.

Violation: An Ostrich inherits a fly method but throws at runtime.

Fix: Model capabilities as smaller abstractions so only flying birds expose fly behavior.

Smell prevented: False inheritance, fragile base classes, and surprise UnsupportedOperationException paths.

Interviewer probe: They add an edge subtype and ask whether the existing workflow still works. If the answer needs type checks, the hierarchy is probably wrong.

abstract class Bird {
    abstract void fly();
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException("Ostrich cannot fly");
    }
}

void migrate(Bird bird) {
    bird.fly();
}

interface BirdFixed {
    void eat();
}

interface FlyingBird extends BirdFixed {
    void fly();
}

class Sparrow implements FlyingBird {
    public void eat() { }
    public void fly() { }
}

class OstrichFixed implements BirdFixed {
    public void eat() { }
}

void migrateFixed(FlyingBird bird) {
    bird.fly();
}

ISP - Interface Segregation Principle

Definition: Clients should not be forced to depend on methods they do not use.

Violation: A simple printer must implement scan and fax even though it cannot do either.

Fix: Split the fat interface into capability interfaces and depend only on the role required by the workflow.

Smell prevented: Fat interfaces, dummy methods, and clients that break when unrelated operations change.

Interviewer probe: They add a low-end device or a read-only client. Your interfaces should let that client implement only the capability it actually has.

interface Machine {
    void print(Document document);
    void scan(Document document);
    void fax(Document document);
}

class SimplePrinter implements Machine {
    public void print(Document document) { }
    public void scan(Document document) {
        throw new UnsupportedOperationException("Scan not supported");
    }
    public void fax(Document document) {
        throw new UnsupportedOperationException("Fax not supported");
    }
}

interface Printer {
    void print(Document document);
}

interface ScannerDevice {
    void scan(Document document);
}

interface FaxDevice {
    void fax(Document document);
}

class SimplePrinterFixed implements Printer {
    public void print(Document document) { }
}

class AllInOneMachine implements Printer, ScannerDevice, FaxDevice {
    public void print(Document document) { }
    public void scan(Document document) { }
    public void fax(Document document) { }
}

DIP - Dependency Inversion Principle

Definition: High-level modules should depend on abstractions, not concrete low-level details.

Violation: The checkout workflow constructs a SQL repository and an SMTP sender directly.

Fix: Define ports for persistence and notification, then inject concrete adapters at the boundary.

Smell prevented: Tight coupling, hard-to-test services, and business logic that changes because infrastructure changes.

Interviewer probe: They ask you to replace SQL with a document store, call a queue instead of email, or write unit tests without real infrastructure. DIP makes those changes local.

class CheckoutService {
    private final SqlOrderRepository repository = new SqlOrderRepository();
    private final SmtpReceiptSender sender = new SmtpReceiptSender();

    Receipt checkout(Order order) {
        repository.save(order);
        Receipt receipt = Receipt.from(order);
        sender.send(receipt);
        return receipt;
    }
}

interface OrderRepository {
    void save(Order order);
}

interface ReceiptSenderPort {
    void send(Receipt receipt);
}

class CheckoutServiceFixed {
    private final OrderRepository repository;
    private final ReceiptSenderPort sender;

    CheckoutServiceFixed(OrderRepository repository, ReceiptSenderPort sender) {
        this.repository = repository;
        this.sender = sender;
    }

    Receipt checkout(Order order) {
        repository.save(order);
        Receipt receipt = Receipt.from(order);
        sender.send(receipt);
        return receipt;
    }
}

Diagrams

DIP example - checkout depends on ports

Rendering diagram…
The high-level checkout workflow owns the use case, but it only knows payment and receipt abstractions. Stripe, fake test gateways, and email senders are replaceable adapters.

Comparisons

SOLID principles mapped to design decisions

PrincipleDesign smell it addressesLLD decision it pushesPattern that often helps
SRPGod object and shotgun surgerySplit domain rule, orchestration, persistence, and notification responsibilitiesFacade around a cohesive subsystem
OCPSwitch explosion and regression-prone central editsMove changing policies behind an extension pointStrategy
LSPFalse inheritance and runtime capability failuresUse capability interfaces or composition instead of forced base classesAdapter or composition over inheritance
ISPFat interfaces and dummy methodsCreate role-focused interfaces per client needRole interface
DIPTight coupling to databases, APIs, and frameworksMake high-level workflows depend on ports and inject adaptersDependency Injection and Repository

Best Practices

  • Name the change axis before introducing an abstraction; abstractions without a likely variant add noise.

  • Use Strategy when interviewers describe a policy family such as pricing, routing, scoring, or matching.

  • Prefer small capability interfaces over one large service interface shared by every client.

  • Use constructor injection for required collaborators so services are testable and valid when created.

  • Keep domain objects free from database, network, clock, and UI details unless those details are the domain itself.

  • Validate inheritance by asking whether every subclass can honor the base contract without special cases.

Common Mistakes

  • ×

    Reciting the acronym without tying each principle to a concrete class boundary.

  • ×

    Adding interfaces for every class even when there is no alternate implementation or test seam.

  • ×

    Using inheritance for code reuse when the subclasses do not share the same behavioral contract.

  • ×

    Letting one manager class coordinate validation, persistence, notification, pricing, and reporting.

  • ×

    Putting every method into a single interface because it feels simpler at first.

  • ×

    Creating objects with new inside high-level workflows, then struggling to unit test or swap infrastructure.

Quiz

0/7 answered

  1. 1.Which statement best captures SRP?

  2. 2.A checkout service uses a switch to calculate Card, UPI, Wallet, and Cash payment fees. Which principle is most directly stressed?

  3. 3.A subclass overrides a base method by throwing UnsupportedOperationException because it cannot support that behavior. What is the likely issue?

  4. 4.A client that only prints documents is forced to implement scan and fax methods. Which principle addresses this?

  5. 5.Why does DIP improve testability?

  6. 6.Which LLD choice most directly supports OCP for pricing rules?

  7. 7.When should you avoid adding an interface?

Flashcards

Cheat Sheet

SRP: One reason to change. Prevents God objects. Split validation, orchestration, persistence, and notification.

OCP: Open for extension, closed for modification. Prevents switch explosion. Use Strategy for changing policies.

LSP: Subtypes must honor the base contract. Prevents false inheritance. Prefer capability interfaces when behavior is optional.

ISP: Clients should not depend on unused methods. Prevents fat interfaces. Split interfaces by role.

DIP: High-level modules depend on abstractions. Prevents infrastructure coupling. Inject repositories, gateways, and senders through ports.

Interview move: For every likely future requirement, name the change axis, the smell it would create, and the SOLID boundary that keeps the change local.

References

  • BookAgile Software Development, Principles, Patterns, and PracticesRobert C. Martin
  • BookDesign Patterns: Elements of Reusable Object-Oriented SoftwareErich Gamma, Richard Helm, Ralph Johnson, John Vlissides
  • DocsRefactoring Guru - SOLID Principles