Stage 6: Design and clean code, lesson 2 of 4

SOLID principles

Intermediate3 min readall versions
Explain it forThe essentials plus production detail and pitfalls.
  • Single responsibility (S): a class should have one reason to change. An InvoiceService shouldn't also send emails and format PDFs.
  • Open/closed (O): add behaviour by adding code, not by editing working code. A new payment method is a new class that implements PaymentGateway.
  • Liskov substitution (L): a subclass must work wherever its parent is expected. If Square extends Rectangle breaks setWidth, the hierarchy is wrong.
  • Interface segregation (I): prefer several small interfaces to one large one, so classes don't implement methods they don't need.
  • Dependency inversion (D): depend on abstractions and let something else (such as Spring) supply the implementation. Constructor injection does exactly this.

SOLID isn't a checklist to apply everywhere; it's a set of questions to ask when code becomes hard to change.

Example

Java
// Dependency inversion + open/closed: add gateways without touching checkout
public interface PaymentGateway {
    String charge(long paise, String orderId);
}

public class RazorpayGateway implements PaymentGateway {
    public String charge(long paise, String orderId) { /* call Razorpay */ return "pay_1"; }
}

public class UpiIntentGateway implements PaymentGateway {       // new behaviour = new class
    public String charge(long paise, String orderId) { /* UPI intent flow */ return "upi_1"; }
}

interface ReceiptSender {                                        // small, focused interface
    void send(Order order, String paymentId);
}

public class CheckoutService {                                   // one responsibility
    private final PaymentGateway gateway;                         // depends on the abstraction
    private final ReceiptSender receipts;

    public CheckoutService(PaymentGateway gateway, ReceiptSender receipts) {
        this.gateway = gateway;
        this.receipts = receipts;
    }

    public void checkout(Order order) {
        String paymentId = gateway.charge(order.totalPaise(), order.id());
        receipts.send(order, paymentId);                          // emailing lives elsewhere
    }
}

Common mistake

Creating an interface for every class "for SOLID". Abstractions have a cost; add them where variation is real.

Under the hood

Over-applying SOLID produces interfaces with a single implementation and layers that only pass calls along. A useful rule: introduce an abstraction when there is (or will clearly be) a second implementation, or when you need to swap it in tests. Liskov violations often show up as instanceof checks or UnsupportedOperationException in subclasses.

Check yourself

Which principle says a class should have only one reason to change?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of Crack the Java interview.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.