Skip to content

SOLID Principles

SOLID is a set of 5 design principles for maintainable OOP code: S — Single Responsibility (one reason to change), O — Open/Closed (open for extension, closed for modification), L — Liskov Substitution (subtypes must be substitutable), I — Interface Segregation (prefer small, focused interfaces), D — Dependency Inversion (depend on abstractions, not concretions). Spring naturally encourages all five.


Single Responsibility Principle (SRP)

A class should have only one reason to change — it should do one thing and do it well. If a class handles user creation, email sending, AND reporting, a change to email logic could break user creation. Separate concerns into focused classes.

Deep Dive: SRP Example

Violation:

public class UserService {
    public void createUser(User user) { ... }
    public void sendWelcomeEmail(User user) { ... }  // Email concern
    public String generateReport(List<User> users) { ... }  // Reporting concern
}

Fixed — each class has one responsibility:

public class UserService {
    public void createUser(User user) { ... }
}

public class EmailService {
    public void sendWelcomeEmail(User user) { ... }
}

public class ReportService {
    public String generateReport(List<User> users) { ... }
}

In Spring: The @Service, @Repository, @Controller stereotype annotations naturally enforce SRP by separating business logic, data access, and web concerns.


Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification. Instead of adding if-else branches for new behavior, use abstractions (interfaces/abstract classes) so new behavior is added by creating new implementations, without modifying existing code.

Deep Dive: OCP Example

Violation — must modify class for every new discount type:

public class DiscountCalculator {
    public double calculate(String type, double price) {
        if (type.equals("REGULAR")) return price * 0.1;
        if (type.equals("PREMIUM")) return price * 0.2;
        if (type.equals("VIP")) return price * 0.3;  // Must modify class!
        return 0;
    }
}

Fixed — extend via abstraction (Strategy Pattern):

public interface DiscountStrategy {
    double calculate(double price);
}

public class RegularDiscount implements DiscountStrategy {
    public double calculate(double price) { return price * 0.1; }
}

public class PremiumDiscount implements DiscountStrategy {
    public double calculate(double price) { return price * 0.2; }
}

// Adding VIP? Just create a new class — no modification needed
public class VipDiscount implements DiscountStrategy {
    public double calculate(double price) { return price * 0.3; }
}

In Spring: @ConditionalOn* annotations, profiles, and DI-based strategy injection all follow OCP.


Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering program correctness. If substituting a subclass for a parent class breaks behavior, the inheritance hierarchy is wrong. Classic violation: Square extending Rectangle — setting width/height independently breaks the contract.

Deep Dive: LSP Violation — Square vs Rectangle
public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) { this.width = w; this.height = w; }
    @Override
    public void setHeight(int h) { this.width = h; this.height = h; }
}
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
assert r.area() == 50;  // FAILS! area is 100 — LSP violated

Fix — don't use inheritance here:

public interface Shape {
    int area();
}

public record Rectangle(int width, int height) implements Shape {
    public int area() { return width * height; }
}

public record Square(int side) implements Shape {
    public int area() { return side * side; }
}


Interface Segregation Principle (ISP)

No client should be forced to depend on methods it doesn't use. Prefer many small, focused interfaces to one large "fat" interface. If a Robot implements Worker but has to stub out eat() and sleep(), the interface is too broad — split it.

Deep Dive: ISP Example

Violation — fat interface:

public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class Robot implements Worker {
    public void work() { ... }
    public void eat() { /* Robots don't eat! */ }   // Forced to implement
    public void sleep() { /* Robots don't sleep! */ }
}

Fixed — segregated interfaces:

public interface Workable { void work(); }
public interface Feedable { void eat(); }
public interface Restable { void sleep(); }

public class Human implements Workable, Feedable, Restable { ... }
public class Robot implements Workable { ... }  // Only what it needs

In Spring: Repository interfaces are focused — CrudRepository, PagingAndSortingRepository, JpaRepository let you choose the level of functionality you need.


Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules — both should depend on abstractions. Code should depend on interfaces, not concrete classes. This is the theoretical foundation for Dependency Injection (DI) — Spring's core mechanism.

Deep Dive: DIP vs DI (Dependency Injection)

DIP is the principle — depend on abstractions.

DI is the mechanism — inject dependencies from outside rather than creating them.

Violation — direct dependency on concrete class:

public class OrderService {
    private MySQLOrderRepository repository = new MySQLOrderRepository();
    // Tightly coupled to MySQL — can't swap to Mongo without changing this
}

Fixed — depend on abstraction + inject:

public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(Long id);
}

public class MySQLOrderRepository implements OrderRepository { ... }
public class MongoOrderRepository implements OrderRepository { ... }

public class OrderService {
    private final OrderRepository repository;

    // Constructor injection — depends on abstraction
    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

Spring does this automatically: @Autowired / constructor injection wires the concrete implementation at runtime.


SOLID in Spring

Deep Dive: How Spring Enforces SOLID
Principle Spring Support
SRP @Service, @Repository, @Controller separate concerns
OCP Strategy pattern via DI, @ConditionalOn* for extension
LSP Interface-based programming, swappable implementations
ISP Multiple focused repository/service interfaces
DIP Constructor injection, program to interfaces

Common Interview Questions

Common Interview Questions
  • Explain each SOLID principle with an example.
  • Give a real-world example where you applied SRP.
  • How does the Open/Closed principle relate to the Strategy pattern?
  • What is the Liskov Substitution Principle? Give a violation example.
  • How does Spring Framework help enforce SOLID principles?
  • What's the difference between DIP and DI (Dependency Injection)?