SOLID Principles in Java (With Real life Examples)

Chhavi RanaChhavi Rana
6 min read

In this article, you'll explore the SOLID principles of object-oriented programming, applied to a real-world online food ordering system. With each principle, you'll get a clear explanation, why it matters, and a Java example that feels practical and relevant.

SOLID principles are a set of five design principles used in object-oriented programming. Adhering to these principles helps you develop robust, maintainable, and readable software.

🚀 What is SOLID? SOLID is an acronym for:

Single Responsibility Principle Open/Closed Principle Liskov Substitution Principle Interface Segregation Principle Dependency Inversion Principle

Let's go through each one with Java examples 👇

1️⃣ Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a component should have one clear purpose or responsibility. It should focus on specific functionality or behavior and avoid taking on unrelated tasks.

Bad Example:

public class OrderService {
    public void placeOrder() { /* ... */ }
    public void generateInvoice() { /* ... */ }
    public void sendConfirmationEmail() { /* ... */ }
}

This class is doing too much: order placement, invoicing, and notification.

Better:

public class OrderService {
    public void placeOrder() { /* ... */ }
}

public class InvoiceService {
    public void generateInvoice() { /* ... */ }
}

public class NotificationService {
    public void sendConfirmationEmail() { /* ... */ }
}

Now, each class has a clear and focused responsibility.


2️⃣ Open/Closed Principle (OCP)

The Open-Closed Principle emphasizes that components should be open for extension (can add new behaviors or functionalities) but closed for modification(existing code should remain unchanged). This principle encourages the creation of code that is resilient to change, modular, and easily maintainable.

Let’s say we’re adding multiple payment methods.

Bad Example:

public class PaymentService {
    public void pay(String method) {
        if (method.equals("CARD")) {
            // card payment logic
        } else if (method.equals("UPI")) {
            // UPI payment logic
        }
    }
}

Adding a new payment type (e.g., Wallet) requires changing this class.

Better: Use polymorphism to extend functionality:

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

public class CardPayment implements PaymentMethod {
    public void pay(double amount) {
        // card payment logic
    }
}

public class UpiPayment implements PaymentMethod {
    public void pay(double amount) {
        // UPI payment logic
    }
}

public class PaymentService {
    public void processPayment(PaymentMethod paymentMethod, double amount) {
        paymentMethod.pay(amount);
    }
}

New payment methods can now be added without modifying PaymentService.


3️⃣ Liskov Substitution Principle (LSP)

A subclass should be usable in place of its parent class without causing errors or unexpected behavior.

Bad Example:

public class DeliveryVehicle {
    public void startEngine() { /* ... */ }
}

public class Bicycle extends DeliveryVehicle {
    public void startEngine() {
        throw new UnsupportedOperationException("Bicycles don't have engines");
    }
}

Now we’ve violated LSP. Why?

A subclass (Bicycle) doesn't honor the contract defined by its parent (DeliveryVehicle).

Replacing the parent class with its child leads to unexpected behavior or runtime exceptions.

Better: The mistake is assuming all delivery modes need to “start an engine”. Instead, we should split the behavior more appropriately:

public abstract class DeliveryMode {
    public abstract void deliver();
}

public class BikeDelivery extends DeliveryMode {
    public void deliver() {
        System.out.println("Delivering via bike");
    }
}

public class ScooterDelivery extends DeliveryMode {
    public void deliver() {
        System.out.println("Delivering via scooter");
    }
}

Now all delivery modes conform to a consistent, expected behavior.


4️⃣ Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) suggests that interfaces should be focused and tailored to specific client requirements rather than being overly broad and forcing clients to implement unnecessary functionality.

Bad Example:

public interface RestaurantPartner {
    void acceptOrder();
    void provideNutritionalInfo();
    void handleFeedback();
}

public class SmallLocalDiner implements RestaurantPartner {
    public void acceptOrder() { /* ... */ }
    public void provideNutritionalInfo() {
        throw new UnsupportedOperationException();
    }
    public void handleFeedback() { /* ... */ }
}

Smaller local eateries might not track nutritional info.

Better: This interface should be split into smaller interfaces.

public interface OrderAcceptance {
    void acceptOrder();
}

public interface FeedbackHandler {
    void handleFeedback();
}

public interface NutritionInfoProvider {
    void provideNutritionalInfo();
}

public class SmallLocalDiner implements OrderAcceptance, FeedbackHandler {
    public void acceptOrder() { /* ... */ }
    public void handleFeedback() { /* ... */ }
}

The OrderAcceptance, FeedbackHandler, and NutritionInfoProvider interfaces are now segregated, ensuring that classes only implement the interfaces they use. This adheres to the Interface Segregation Principle.

Now each restaurant can implement only what they support.


5️⃣ Dependency Inversion Principle (DIP)

High-level modules should depend on abstractions, not concrete implementations.

Bad Example:

public class OrderNotifier {
    private EmailService emailService = new EmailService();

    public void notifyCustomer(String message) {
        emailService.sendEmail(message);
    }
}

OrderNotifier class directly depends on the EmailService class, making it tightly coupled. If we want to switch to a different notification method (like SMS), we would need to modify the OrderNotifier class, which violates the Open/Closed Principle (OCP)

Better: Now, let’s refactor the code to adhere to the Dependency Inversion Principle by introducing an abstraction (interface).

// Notifier.java (Abstraction)
public interface Notifier {
    void notify(String message);
}

// EmailService.java (Low-level implementation)
public class EmailService implements Notifier {
    public void notify(String message) {
        System.out.println("Email: " + message);
    }
}

// SmsService.java (Low-level implementation)
public class SmsService implements Notifier {
    public void notify(String message) {
        System.out.println("SMS: " + message);
    }
}

// OrderNotifier.java (High-level module)
public class OrderNotifier {
    private Notifier notifier;

    // Dependency injection (through constructor)
    public OrderNotifier(Notifier notifier) {
        this.notifier = notifier;
    }

    public void notifyCustomer(String message) {
        notifier.notify(message); // Use the abstraction (Notifier)
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Notifier emailService = new EmailService(); // Can easily switch to SmsService
        OrderNotifier orderNotifier = new OrderNotifier(emailService); // Inject dependency
        orderNotifier.notifyCustomer("Order Shipped!");
    }
}

The OrderNotifier class no longer directly depends on EmailService. Instead, it depends on the Notifier interface, which is an abstraction.

Low-level modules (like EmailService and SmsService) now implement the abstraction (Notifier interface), not concrete classes.

The high-level module (OrderNotifier) is decoupled from the specific notification method and can work with any class that implements the Notifier interface.

This makes it extensible and flexible: if we want to add new notification methods, like Push Notifications, we just need to create a new class implementing Notifier without modifying OrderNotifier.


✅ Conclusion

The SOLID principles aren’t just theory — they are practical tools for writing clean, scalable, and maintainable code.

Using the real-world context of a food ordering app, we’ve seen how:

SRP improves modularity OCP makes your system extensible LSP ensures reliable inheritance ISP keeps interfaces clean and focused DIP decouples high-level and low-level logic

Applying SOLID isn’t about adding complexity — it's about writing better code that stands the test of time. 💻✨

3
Subscribe to my newsletter

Read articles from Chhavi Rana directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chhavi Rana
Chhavi Rana