SSourav Saha
HomeExperienceSoftware DesignSystem DesignLearningBooksToolsContact
SSourav Saha

Building scalable backend systems, distributed infrastructure, and cloud-native applications.

Navigation

  • Home
  • Experience
  • Software Design
  • System Design
  • Learning

More

  • Books
  • Tools
  • Contact

Connect

  • LinkedIn
  • Email

© 2026 Sourav Saha. All rights reserved.

Built with using Next.js

Software DesignThe SOLID Principles Explained with Java Examples
Software DesignSOLID

The SOLID Principles Explained with Java Examples

A deep dive into SRP, OCP, LSP, ISP, and DIP — the five principles that make object-oriented systems scalable, testable, and maintainable.

May 17, 20246 min read
solidooparchitecturejava

Introduction

SOLID is an acronym for five design principles that help developers build software that is easy to maintain, extend, and test. These principles were popularized by Robert C. Martin (Uncle Bob) and are the foundation of professional object-oriented design.


S — Single Responsibility Principle (SRP)

A class should have only one reason to change.

Bad Code

public class UserService {
    public void registerUser(User user) {
        // Validate
        if (user.getEmail() == null) throw new IllegalArgumentException("Email required");
        
        // Save to DB
        Connection conn = DriverManager.getConnection("...");
        PreparedStatement ps = conn.prepareStatement("INSERT INTO users ...");
        ps.executeUpdate();
        
        // Send welcome email
        EmailClient client = new EmailClient();
        client.send(user.getEmail(), "Welcome!", "...");
        
        // Log audit
        AuditLogger.log("User registered: " + user.getEmail());
    }
}

Problems: This class changes if validation rules change, if the database changes, if email templates change, or if audit requirements change. Four reasons to change.

Refactored Code

public class UserRegistrationService {
    private final UserValidator validator;
    private final UserRepository userRepository;
    private final WelcomeEmailSender emailSender;
    private final AuditLogger auditLogger;

    public void registerUser(User user) {
        validator.validate(user);
        userRepository.save(user);
        emailSender.sendWelcomeEmail(user);
        auditLogger.logRegistration(user);
    }
}

Each collaborator has exactly one reason to change.


O — Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

Bad Code

public class PaymentProcessor {
    public void processPayment(String type, double amount) {
        if ("UPI".equals(type)) {
            // UPI logic
        } else if ("CARD".equals(type)) {
            // Card logic
        } else if ("NET_BANKING".equals(type)) {
            // Net banking logic
        }
        // Every new payment method = modify this class
    }
}

Refactored Code

public interface PaymentStrategy {
    void pay(double amount);
}

public class UpiPayment implements PaymentStrategy {
    public void pay(double amount) { /* UPI logic */ }
}

public class CardPayment implements PaymentStrategy {
    public void pay(double amount) { /* Card logic */ }
}

public class NetBankingPayment implements PaymentStrategy {
    public void pay(double amount) { /* Net banking logic */ }
}

public class PaymentProcessor {
    private final PaymentStrategy strategy;

    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void processPayment(double amount) {
        strategy.pay(amount);
    }
}

Adding a new payment method (e.g., Crypto) means creating a new class — zero modification to existing code.


L — Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering program correctness.

Classic Example: Bird / Penguin

public class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

This violates LSP. Any code that calls bird.fly() will break if it receives a Penguin.

Refactored Code

public abstract class Bird {
    public abstract void move();
}

public class Sparrow extends Bird {
    public void move() { System.out.println("Flying..."); }
}

public class Penguin extends Bird {
    public void move() { System.out.println("Swimming..."); }
}

Real-World Example: Payment Gateway

// Bad — violates LSP
public class FreeTrialAccount extends PaymentAccount {
    @Override
    public void charge(double amount) {
        throw new UnsupportedOperationException("Free trial can't be charged");
    }
}

// Good — separate interfaces
public interface Chargeable {
    void charge(double amount);
}

public interface AccountInfo {
    String getAccountId();
    String getPlan();
}

public class PaidAccount implements Chargeable, AccountInfo { ... }
public class FreeTrialAccount implements AccountInfo { ... }

I — Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use.

Bad Code

public interface Employee {
    void work();
    void attendMeeting();
    void generateReport();
    void codeReview();
    void manageSprint();
    void conductInterview();
    void approveLeave();
}

A junior developer is forced to implement approveLeave() and manageSprint(), which they never do.

Refactored Code

public interface Worker {
    void work();
}

public interface MeetingAttendee {
    void attendMeeting();
}

public interface SprintManager {
    void manageSprint();
    void approveLeave();
}

public interface CodeReviewer {
    void codeReview();
}

// A junior implements only what they need
public class JuniorDeveloper implements Worker, MeetingAttendee { ... }

// A tech lead implements more
public class TechLead implements Worker, MeetingAttendee, CodeReviewer, SprintManager { ... }

D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Evolution: From Tight Coupling to Spring Boot

Step 1 — Tight Coupling (Bad)

public class NotificationService {
    private EmailSender emailSender = new EmailSender(); // Hardcoded dependency

    public void notify(String message) {
        emailSender.send(message);
    }
}

Step 2 — Manual Dependency Injection

public class NotificationService {
    private final MessageSender sender;

    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }

    public void notify(String message) {
        sender.send(message);
    }
}

// Usage
NotificationService service = new NotificationService(new EmailSender());
NotificationService smsService = new NotificationService(new SmsSender());

Step 3 — Spring Boot DI

public interface MessageSender {
    void send(String message);
}

@Component
public class EmailSender implements MessageSender {
    public void send(String message) { /* send email */ }
}

@Service
public class NotificationService {
    private final MessageSender sender;

    @Autowired
    public NotificationService(MessageSender sender) {
        this.sender = sender;
    }
}

Step 4 — Testability

@Test
void shouldSendNotification() {
    MessageSender mockSender = mock(MessageSender.class);
    NotificationService service = new NotificationService(mockSender);
    
    service.notify("Hello");
    
    verify(mockSender).send("Hello");
}

DIP enables testability. You can inject a mock instead of a real email sender.


Interview Questions

  1. Which SOLID principle is most important? They are all interconnected, but SRP is the foundation. If each class has one responsibility, the others naturally follow.

  2. How does OCP relate to design patterns? Strategy, Observer, and Decorator patterns are direct implementations of OCP — they allow extending behavior without modifying existing code.

  3. Give a real-world LSP violation. Java's Stack extending Vector. Stack is LIFO, but inheriting from Vector exposes add(index, element) which breaks the LIFO contract.


Common Mistakes

  • Over-abstracting: Creating interfaces for classes that will only ever have one implementation. SOLID is about managing complexity, not creating it.
  • Applying all principles at once: Start with SRP. The rest become natural as the codebase grows.
  • Ignoring DIP in tests: If your unit tests need a running database or email server, you have a DIP violation.

Summary

PrincipleOne-Liner
SRPOne class, one reason to change
OCPExtend behavior without modifying code
LSPSubtypes must be drop-in replacements
ISPSmall, focused interfaces
DIPDepend on abstractions, not concretions

Related Articles

  • Clean Code Principles
  • Core OOP Principles
  • Practical Design Patterns
PreviousRefactoring Techniques That Actually WorkNextCore OOP Principles for Scalable Systems