System Design

SOLID is a set of five principles for designing software that is easy to understand, maintain, and extend. These principles are often used in object-oriented design and serve as guidelines for creating flexible and robust systems.

SOLID Principles

  1. Single Responsibility Principle (SRP)

  2. Open/Closed Principle (OCP)

  3. Liskov Substitution Principle (LSP)

  4. Interface Segregation Principle (ISP)

  5. Dependency Inversion Principle (DIP)

These principles make systems more modular, maintainable, and scalable, reducing the chances of bugs and simplifying future development.

1. Single Responsibility Principle (SRP)

Principle: A class should have only one reason to change, i.e., it should have only one responsibility.

In a car rental service, we could have a CarService that manages car-related logic (like availability) and a separate RentalService that handles rental-related logic.

Code Example:

// CarService.java
@Service
public class CarService {
    public boolean isCarAvailable(String carId) {
        // Check if the car is available for rent
        return true; // Assume car is available
    }
}

// RentalService.java
@Service
public class RentalService {
    private final CarService carService;

    public RentalService(CarService carService) {
        this.carService = carService;
    }

    public void rentCar(String carId, String userId) {
        if (carService.isCarAvailable(carId)) {
            // Logic to rent the car
            System.out.println("Car rented successfully.");
        } else {
            System.out.println("Car not available.");
        }
    }
}

Explanation: CarService only checks the car's availability, while RentalService handles the rental logic. Each service has a single responsibility.


2. Open/Closed Principle (OCP)

Principle: Classes should be open for extension but closed for modification.

In a car rental service, suppose we want to calculate different rental prices based on car types. Instead of modifying a single pricing class each time we add a new car type, we can extend it.

Code Example:

// RentalPriceCalculator.java
public interface RentalPriceCalculator {
    double calculatePrice(int days);
}

// StandardCarPriceCalculator.java
@Service
public class StandardCarPriceCalculator implements RentalPriceCalculator {
    public double calculatePrice(int days) {
        return days * 50; // Standard car rental price
    }
}

// LuxuryCarPriceCalculator.java
@Service
public class LuxuryCarPriceCalculator implements RentalPriceCalculator {
    public double calculatePrice(int days) {
        return days * 100; // Luxury car rental price
    }
}

Explanation: If a new car type is added, like SportsCarPriceCalculator, we create a new class implementing RentalPriceCalculator without modifying the existing ones.


3. Liskov Substitution Principle (LSP)

Principle: Objects of a superclass should be replaceable with objects of its subclasses without affecting the system.

In our car rental system, if ElectricCar is a subtype of Car, it should work in the same way as a Car when rented, without breaking any functionality.

Code Example:

// Car.java
public abstract class Car {
    public abstract void start();
}

// ElectricCar.java
@Service
public class ElectricCar extends Car {
    public void start() {
        System.out.println("Electric car started silently.");
    }
}

// DieselCar.java
@Service
public class DieselCar extends Car {
    public void start() {
        System.out.println("Diesel car started with engine sound.");
    }
}

Explanation: Both ElectricCar and DieselCar can be used wherever a Car type is expected without breaking the code, thus adhering to Liskov Substitution.


4. Interface Segregation Principle (ISP)

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

In our car rental example, a Car interface could have separate interfaces like FuelCar and ElectricCar. This way, electric cars don’t need to implement fuel-related methods.

Code Example:

// Car.java
public interface Car {
    void start();
}

// FuelCar.java
public interface FuelCar extends Car {
    void refuel();
}

// ElectricCar.java
public interface ElectricCar extends Car {
    void recharge();
}

// Sedan.java
@Service
public class Sedan implements FuelCar {
    public void start() {
        System.out.println("Sedan car started.");
    }

    public void refuel() {
        System.out.println("Sedan refueled.");
    }
}

// Tesla.java
@Service
public class Tesla implements ElectricCar {
    public void start() {
        System.out.println("Tesla started.");
    }

    public void recharge() {
        System.out.println("Tesla recharged.");
    }
}

Explanation: FuelCar has a refuel() method, while ElectricCar has a recharge() method. Tesla, being an electric car, does not need to implement refuel().


5. Dependency Inversion Principle (DIP)

Principle: High-level modules should not depend on low-level modules but rather on abstractions.

In our car rental system, the RentalService should depend on an NotificationService interface rather than a specific implementation like EmailNotificationService.

Code Example:

// NotificationService.java
public interface NotificationService {
    void notifyUser(String message);
}

// EmailNotificationService.java
@Service
public class EmailNotificationService implements NotificationService {
    public void notifyUser(String message) {
        System.out.println("Email notification sent: " + message);
    }
}

// RentalService.java
@Service
public class RentalService {
    private final NotificationService notificationService;

    public RentalService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void rentCar(String carId, String userId) {
        // Rent the car and notify user
        notificationService.notifyUser("Your car rental is confirmed.");
    }
}

Explanation: RentalService depends on the NotificationService abstraction, so we can switch to any other notification implementation, like SMSNotificationService, without changing RentalService.


Summary Table

SOLID Principle
Car Rental Service Example

Single Responsibility

CarService checks availability, RentalService handles rentals.

Open/Closed

RentalPriceCalculator extended by StandardCarPriceCalculator, etc.

Liskov Substitution

ElectricCar and DieselCar are interchangeable as Car types.

Interface Segregation

FuelCar and ElectricCar interfaces avoid unused methods.

Dependency Inversion

RentalService depends on NotificationService abstraction.

This unified car rental example demonstrates each SOLID principle in Spring Boot for a maintainable, scalable system.

Last updated