SOLID Principles in Java: Building Maintainable and Scalable Applications

algo

Introduction

As software systems grow, code often becomes difficult to maintain, extend, and test. New features introduce unexpected bugs, small changes require modifications in multiple places, and development velocity gradually decreases.

To address these challenges, Robert C. Martin (Uncle Bob) introduced the SOLID Principles, a set of five object-oriented design principles that help developers create software that is:

  • Easy to understand
  • Easy to extend
  • Easy to test
  • Easy to maintain
  • Resilient to change

Whether you’re building a simple Spring Boot application or a large-scale microservices platform, understanding SOLID principles is essential for every Java developer.


What is SOLID?

SOLID is an acronym representing five design principles:

PrincipleMeaning
SSingle Responsibility Principle
OOpen Closed Principle
LLiskov Substitution Principle
IInterface Segregation Principle
DDependency Inversion Principle

Let’s explore each principle with Java examples.


S – Single Responsibility Principle (SRP)

Definition

A class should have only one reason to change.

A class should be responsible for only one piece of functionality.


Bad Example

public class EmployeeService {

    public void saveEmployee(Employee employee) {
        // Save employee
    }

    public void generateReport(Employee employee) {
        // Generate PDF report
    }

    public void sendEmail(Employee employee) {
        // Send email
    }
}

Problems

The class has multiple responsibilities:

  • Employee persistence
  • Report generation
  • Email notification

Changes in any of these areas impact the same class.


Good Example

public class EmployeeService {

    public void saveEmployee(Employee employee) {
        // Save employee
    }
}
public class ReportService {

    public void generateReport(Employee employee) {
        // Generate report
    }
}
public class EmailService {

    public void sendEmail(Employee employee) {
        // Send email
    }
}

Each class now has a single responsibility.


Spring Boot Example

Common services:

@Service
public class CustomerService {}

@Service
public class NotificationService {}

@Service
public class AuditService {}

Each service handles one concern.


O – Open Closed Principle (OCP)

Definition

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

We should be able to add new behavior without changing existing code.


Bad Example

public class PaymentService {

    public void pay(String type) {

        if(type.equals("CARD")) {
            System.out.println("Card Payment");
        }
        else if(type.equals("UPI")) {
            System.out.println("UPI Payment");
        }
    }
}

Every new payment method requires modification.


Good Example Using Strategy Pattern

Step 1

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

Step 2

public class CardPayment implements PaymentStrategy {

    @Override
    public void pay(double amount) {
        System.out.println("Card Payment");
    }
}
public class UpiPayment implements PaymentStrategy {

    @Override
    public void pay(double amount) {
        System.out.println("UPI Payment");
    }
}

Step 3

public class PaymentService {

    private final PaymentStrategy strategy;

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

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

Adding a new payment type requires creating a new implementation only.


Real World Examples

  • Spring Security Authentication Providers
  • Message Converters
  • Notification Channels
  • Payment Gateways

L – Liskov Substitution Principle (LSP)

Definition

Objects of a superclass should be replaceable with objects of its subclasses without breaking behavior.


Bad Example

class Bird {

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

    @Override
    public void fly() {
        throw new UnsupportedOperationException();
    }
}

Problem:

A Penguin is not substitutable for Bird.


Better Design

interface Bird {
}
interface FlyingBird extends Bird {
    void fly();
}
class Sparrow implements FlyingBird {

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

Now the hierarchy reflects reality.


Why LSP Matters

Violating LSP causes:

  • Unexpected runtime failures
  • Broken polymorphism
  • Fragile inheritance structures

I – Interface Segregation Principle (ISP)

Definition

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


Bad Example

public interface Worker {

    void work();

    void eat();

    void sleep();
}

A Robot worker doesn’t eat or sleep.

public class RobotWorker implements Worker {

    @Override
    public void work() {}

    @Override
    public void eat() {
        throw new UnsupportedOperationException();
    }

    @Override
    public void sleep() {
        throw new UnsupportedOperationException();
    }
}

Good Example

public interface Workable {
    void work();
}
public interface Eatable {
    void eat();
}
public interface Sleepable {
    void sleep();
}
public class HumanWorker
        implements Workable,
                   Eatable,
                   Sleepable {
}
public class RobotWorker
        implements Workable {
}

Spring Example

Instead of one giant interface:

CrudRepository
PagingAndSortingRepository
JpaRepository

Spring follows ISP extensively.


D – Dependency Inversion Principle (DIP)

Definition

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


Bad Example

public class NotificationService {

    private EmailService emailService =
            new EmailService();

    public void send() {
        emailService.send();
    }
}

Tightly coupled.


Good Example

Abstraction

public interface NotificationChannel {

    void send();
}

Implementations

public class EmailService
        implements NotificationChannel {

    public void send() {
        System.out.println("Email");
    }
}
public class SmsService
        implements NotificationChannel {

    public void send() {
        System.out.println("SMS");
    }
}

Consumer

public class NotificationService {

    private NotificationChannel channel;

    public NotificationService(
            NotificationChannel channel) {

        this.channel = channel;
    }

    public void send() {
        channel.send();
    }
}

Spring Boot Example

@Service
public class NotificationService {

    private final NotificationChannel channel;

    @Autowired
    public NotificationService(
            NotificationChannel channel) {

        this.channel = channel;
    }
}

Spring automatically injects the implementation.


SOLID and Design Patterns

PrincipleCommon Design Pattern
SRPService Layer
OCPStrategy Pattern
LSPProper Inheritance
ISPSmall Interfaces
DIPDependency Injection

SOLID in Microservices

Consider a Notification Microservice:

SRP

Separate services for:

  • Email
  • SMS
  • Push

OCP

Add WhatsApp notifications without changing existing code.

LSP

All notification providers behave consistently.

ISP

Small interfaces:

EmailSender
SmsSender
PushSender

DIP

Business logic depends on:

NotificationChannel

instead of:

EmailProvider
SmsProvider

Common Interview Questions

Q1. What does SOLID stand for?

Single Responsibility, Open Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.


Q2. Which SOLID principle is most commonly violated?

Single Responsibility Principle.


Q3. Which design pattern best demonstrates OCP?

Strategy Pattern.


Q4. Which SOLID principle does Spring Dependency Injection support?

Dependency Inversion Principle.


Q5. Does using interfaces automatically satisfy SOLID?

No.

Poor interface design can still violate ISP and LSP.


Benefits of SOLID Principles

Following SOLID results in:

  • Better maintainability
  • Easier testing
  • Reduced coupling
  • Increased extensibility
  • Better team productivity
  • Easier microservice evolution

Conclusion

SOLID principles are not just interview topics—they are practical guidelines for building maintainable software. Modern frameworks such as Spring Boot, Spring Security, Spring Data JPA, and Microservices architectures heavily rely on these principles.

A good rule of thumb is:

  • Keep classes focused (SRP)
  • Extend behavior without modifying existing code (OCP)
  • Design inheritance carefully (LSP)
  • Create small, focused interfaces (ISP)
  • Depend on abstractions, not implementations (DIP)

Mastering SOLID principles is one of the fastest ways to transition from writing code that works to designing systems that scale and evolve gracefully.

Leave a Reply

Your email address will not be published. Required fields are marked *