SOLID Principles in Java: Building Resilient Software

The Code Quality Standard

SOLID is the definitive guide for writing maintainable software. For a Java developer, these principles are the difference between a “Legacy Nightmare” and a “Clean Architecture.” Understanding these is essential for building scalable Spring Boot or enterprise Jakarta EE applications.

The Five Principles

1. S: Single Responsibility Principle (SRP)

A class should have one, and only one, reason to change.

  • Example: Don’t put SQL logic inside your Domain Entity. Separate business rules from persistence logic.

2. O: Open/Closed Principle (OCP)

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

  • Example: Use interfaces and polymorphism to add new payment methods (e.g., PayPalPayment, StripePayment) without editing the main PaymentProcessor code.

3. L: Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without breaking the application.

  • Example: If Square extends Rectangle, but changing the width of a Square also changes its height, it violates LSP because a Rectangle is expected to have independent dimensions.

4. I: Interface Segregation Principle (ISP)

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

  • Example: Break a large Worker interface into Workable and Feedable so a Robot doesn’t have to implement an eat() method.

5. D: Dependency Inversion Principle (DIP)

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

  • Example: Inject an EmailProvider interface into your service instead of hardcoding a concrete GmailService class.

Practice Exercise: Refactoring a “God Class”

We will refactor an OrderProcessor that handles validation, persistence, and notifications (violating SRP).

The Problem (Violation of SRP)

public class OrderProcessor {
    public void process(Order order) {
        if (order.items().isEmpty()) throw new IllegalArgumentException("No items");

        // Direct DB dependency (Low-level)
        Database.save(order);

        // Direct Email dependency (Low-level)
        EmailService.send("Order processed!");
    }
}

The Optimized Solution (SRP + DIP)

public class OrderProcessor {
    private final OrderRepository repository;
    private final NotificationService notification;

    public OrderProcessor(OrderRepository repository, NotificationService notification) {
        this.repository = repository;
        this.notification = notification;
    }

    public void process(Order order) {
        // Validation is encapsulated in the Domain model
        order.validate();

        repository.save(order);
        notification.notifyUser(order.getCustomerId(), "Order processed!");
    }
}

Why This Works

  • Maintainability: If you switch from SQL to MongoDB, you only replace the OrderRepository implementation. The OrderProcessor remains untouched.
  • Testability: You can easily mock OrderRepository and NotificationService to unit test the processor’s business logic without needing a real database or mail server.
  • Flexibility: New notification types (SMS, Push) can be added simply by implementing the NotificationService interface.

Summary

SOLID principles are not rigid laws, but they are incredibly reliable heuristics. By following them, you create systems that are easy to understand, easy to change, and easy to test—the three hallmarks of professional software engineering.