SOLID Principles in Programming: Understanding with Real Life Examples.

    SOLID Principles in Programming: Understanding with Real Life Examples.

    Learn SOLID principles in Programming with real-life examples. Build clean, maintainable, scalable code for robust applications.

    default profile

    Santosh Mane

    May 12, 2025

    11 min read

    If you're a software developer, you've probably heard of the SOLID principles. They are the backbone of writing clean, maintainable, and scalable code. In this blog, we’ll dive deep into these principles using practical examples from everyday applications like food ordering and ride-booking. Whether you're building a ride-sharing app or a food delivery service, these principles will help you level up your coding game.

    What are SOLID Principles?#

    SOLID is an acronym for five design principles that help developers write clean, readable, and maintainable code. Let’s break them down and explore each one with relatable, real-world examples. For each principle, we’ll cover its definition, a real-world example of a wrong approach, the issues that arise from not following the principle, and a correct approach with a complete example demonstrating how to use it and resolve the issues.

    The five principles are:

    1. Single Responsibility Principle (SRP)
    2. Open/Closed Principle (OCP)
    3. Liskov’s Substitution Principle (LSP)
    4. Interface Segregation Principle (ISP)
    5. Dependency Inversion Principle (DIP)

    1. Single Responsibility Principle (SRP)#

    Definition#

    The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one responsibility or job. This keeps classes focused and easier to maintain.

    Real-World Example: Wrong Approach#

    Imagine you’re building a food delivery app like Uber Eats. You create a FoodOrder class that handles everything: calculating the order total, sending notifications to the customer, and saving the order to the database.

    class FoodOrder { private String orderId; private double amount; private String customerEmail; // Calculate total public double calculateTotal() { // Logic to calculate total return amount; } // Send email notification public void sendNotification() { System.out.println("Sending email to " + customerEmail); } // Save to database public void saveToDatabase() { System.out.println("Saving order " + orderId + " to database"); } }

    Issue if We Don’t Follow SRP#

    This FoodOrder class has multiple responsibilities: order calculation, notification, and database operations. If the email notification logic changes (e.g., switching to SMS), or the database schema changes, you’ll need to modify the FoodOrder class. This increases the risk of introducing bugs and makes the class harder to test and maintain.

    Correct Approach#

    To follow SRP, split the responsibilities into separate classes: one for order calculation, one for notifications, and one for database operations.

    // Class responsible for order details and calculation class FoodOrder { private String orderId; private double amount; public double calculateTotal() { // Logic to calculate total return amount; } // Getters and setters public String getOrderId() { return orderId; } public void setOrderId(String orderId) { this.orderId = orderId; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; } } // Class responsible for sending notifications class NotificationService { public void sendNotification(String customerEmail, String message) { System.out.println("Sending email to " + customerEmail + ": " + message); } } // Class responsible for database operations class OrderRepository { public void saveToDatabase(String orderId) { System.out.println("Saving order " + orderId + " to database"); } } // Usage public class FoodDeliveryApp { public static void main(String[] args) { // Create an order FoodOrder order = new FoodOrder(); order.setOrderId("12345"); order.setAmount(50.0); // Calculate total double total = order.calculateTotal(); System.out.println("Order total: $" + total); // Send notification NotificationService notificationService = new NotificationService(); notificationService.sendNotification("customer@example.com", "Your order is confirmed!"); // Save to database OrderRepository repository = new OrderRepository(); repository.saveToDatabase(order.getOrderId()); } }

    How We Solved the Issue#

    By splitting the responsibilities, each class now has a single reason to change:

    • FoodOrder only changes if the order calculation logic changes.
    • NotificationService only changes if the notification mechanism changes (e.g., adding SMS support).
    • OrderRepository only changes if the database operations change.This makes the code easier to maintain, test, and extend, reducing the risk of bugs.

    2. Open/Closed Principle (OCP)#

    Definition#

    The Open/Closed Principle states that software entities (classes, modules, etc.) should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.

    Real-World Example: Wrong Approach#

    Suppose you’re building a ride-booking app like Lyft. You have a Ride class that calculates the fare based on the ride type (e.g., economy or premium).

    class Ride { private String rideType; public double calculateFare() { if (rideType.equals("economy")) { return 10.0; } else if (rideType.equals("premium")) { return 20.0; } return 0.0; } }

    Issue if We Don’t Follow OCP#

    If you want to add a new ride type (e.g., “shared”), you need to modify the calculateFare method, adding another if condition. This violates OCP because the Ride class is not closed for modification. Every new ride type requires changing the existing code, increasing the risk of errors.

    Correct Approach#

    Use polymorphism to make the system extensible. Define an interface for fare calculation and create separate classes for each ride type.

    // Interface for fare calculation interface RideType { double calculateFare(); } // Economy ride type class EconomyRide implements RideType { public double calculateFare() { return 10.0; } } // Premium ride type class PremiumRide implements RideType { public double calculateFare() { return 20.0; } } // Shared ride type (new addition) class SharedRide implements RideType { public double calculateFare() { return 5.0; } } // Ride class class Ride { private RideType rideType; public Ride(RideType rideType) { this.rideType = rideType; } public double calculateFare() { return rideType.calculateFare(); } } // Usage public class RideBookingApp { public static void main(String[] args) { // Economy ride Ride economyRide = new Ride(new EconomyRide()); System.out.println("Economy ride fare: $" + economyRide.calculateFare()); // Premium ride Ride premiumRide = new Ride(new PremiumRide()); System.out.println("Premium ride fare: $" + premiumRide.calculateFare()); // Shared ride (new type added without modifying Ride class) Ride sharedRide = new Ride(new SharedRide()); System.out.println("Shared ride fare: $" + sharedRide.calculateFare()); } }

    How We Solved the Issue#

    By using an interface (RideType), we made the Ride class open for extension (new ride types can be added by creating new classes) and closed for modification (no need to change the Ride class when adding a new ride type). This makes the system more flexible and less prone to errors.

    3. Liskov Substitution Principle (LSP)#

    Definition#

    The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Subclasses must behave in a way that doesn’t break the expectations set by the superclass.

    Real-World Example: Wrong Approach#

    In an e-commerce platform, you have a Payment class for processing payments. You create a subclass CashPayment that doesn’t require a transaction ID, unlike other payment methods.

    class Payment { public void processPayment() { System.out.println("Processing payment with transaction ID"); } } class CashPayment extends Payment { public void processPayment() { System.out.println("Processing cash payment (no transaction ID)"); } } class PaymentProcessor { public void process(Payment payment) { payment.processPayment(); } }

    Issue if We Don’t Follow LSP#

    The CashPayment class overrides processPayment in a way that changes the expected behavior (no transaction ID). If the PaymentProcessor expects a transaction ID for all payments, using CashPayment could break the system, violating LSP.

    Correct Approach#

    Redesign the class hierarchy to ensure subclasses adhere to the superclass’s contract. Use an interface to define the payment behavior.

    // Interface for payment processing interface Payment { void processPayment(); } // Credit card payment class CreditCardPayment implements Payment { private String transactionId; public CreditCardPayment(String transactionId) { this.transactionId = transactionId; } public void processPayment() { System.out.println("Processing credit card payment with transaction ID: " + transactionId); } } // Cash payment class CashPayment implements Payment { public void processPayment() { System.out.println("Processing cash payment"); } } // Payment processor class PaymentProcessor { public void process(Payment payment) { payment.processPayment(); } } // Usage public class ECommerceApp { public static void main(String[] args) { PaymentProcessor processor = new PaymentProcessor(); // Credit card payment Payment creditCard = new CreditCardPayment("TXN123"); processor.process(creditCard); // Cash payment Payment cash = new CashPayment(); processor.process(cash); } }

    How We Solved the Issue#

    By using an interface (Payment), we ensure that all payment types implement processPayment in a way that’s compatible with the PaymentProcessor. The CashPayment class no longer breaks the expectation of the system, as it provides its own valid implementation. This adheres to LSP and makes the system more robust.

    4. Interface Segregation Principle (ISP)#

    Definition#

    The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. Interfaces should be specific to the needs of the client.

    Real-World Example: Wrong Approach#

    In a food delivery app, you have an OrderService interface that includes methods for placing orders, tracking orders, and canceling orders. Both customers and restaurants implement this interface.

    interface OrderService { void placeOrder(); void trackOrder(); void cancelOrder(); } class Customer implements OrderService { public void placeOrder() { System.out.println("Customer placing order"); } public void trackOrder() { System.out.println("Customer tracking order"); } public void cancelOrder() { System.out.println("Customer canceling order"); } } class Restaurant implements OrderService { public void placeOrder() { throw new UnsupportedOperationException("Restaurant cannot place order"); } public void trackOrder() { System.out.println("Restaurant tracking order"); } public void cancelOrder() { System.out.println("Restaurant canceling order"); } }

    Issue if We Don’t Follow ISP#

    The Restaurant class is forced to implement placeOrder, which it doesn’t need, leading to an UnsupportedOperationException. This violates ISP because the interface is too broad, forcing clients to implement irrelevant methods.

    Correct Approach#

    Split the interface into smaller, specific interfaces that clients can implement as needed.

    // Interface for placing orders interface OrderPlacement { void placeOrder(); } // Interface for tracking orders interface OrderTracking { void trackOrder(); } // Interface for canceling orders interface OrderCancellation { void cancelOrder(); } // Customer implements all interfaces class Customer implements OrderPlacement, OrderTracking, OrderCancellation { public void placeOrder() { System.out.println("Customer placing order"); } public void trackOrder() { System.out.println("Customer tracking order"); } public void cancelOrder() { System.out.println("Customer canceling order"); } } // Restaurant implements only relevant interfaces class Restaurant implements OrderTracking, OrderCancellation { public void trackOrder() { System.out.println("Restaurant tracking order"); } public void cancelOrder() { System.out.println("Restaurant canceling order"); } } // Usage public class FoodDeliveryApp { public static void main(String[] args) { // Customer actions Customer customer = new Customer(); customer.placeOrder(); customer.trackOrder(); customer.cancelOrder(); // Restaurant actions Restaurant restaurant = new Restaurant(); restaurant.trackOrder(); restaurant.cancelOrder(); } }

    How We Solved the Issue#

    By splitting the OrderService interface into smaller interfaces (OrderPlacement, OrderTracking, OrderCancellation), we ensure that clients like Restaurant only implement the methods they need. This adheres to ISP, making the code cleaner and more maintainable.

    5. Dependency Inversion Principle (DIP)#

    Definition#

    The Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. Additionally, abstractions should not depend on details; details should depend on abstractions.

    Real-World Example: Wrong Approach#

    In a ride-booking app, you have a RideService class that directly depends on a PaymentProcessor class for processing payments.

    class PaymentProcessor { public void processPayment() { System.out.println("Processing payment with credit card"); } } class RideService { private PaymentProcessor paymentProcessor; public RideService() { this.paymentProcessor = new PaymentProcessor(); } public void bookRide() { paymentProcessor.processPayment(); System.out.println("Ride booked"); } }

    Issue if We Don’t Follow DIP#

    The RideService class is tightly coupled to the PaymentProcessor class. If you want to support a different payment method (e.g., PayPal), you’ll need to modify the RideService class, violating DIP and making the system less flexible.

    Correct Approach#

    Introduce an abstraction (interface) for payment processing and inject the dependency into the RideService class.

    // Interface for payment processing interface PaymentProcessor { void processPayment(); } // Credit card payment processor class CreditCardProcessor implements PaymentProcessor { public void processPayment() { System.out.println("Processing payment with credit card"); } } // PayPal payment processor class PayPalProcessor implements PaymentProcessor { public void processPayment() { System.out.println("Processing payment with PayPal"); } } // Ride service class RideService { private PaymentProcessor paymentProcessor; public RideService(PaymentProcessor paymentProcessor) { this.paymentProcessor = paymentProcessor; } public void bookRide() { paymentProcessor.processPayment(); System.out.println("Ride booked"); } } // Usage public class RideBookingApp { public static void main(String[] args) { // Book ride with credit card PaymentProcessor creditCardProcessor = new CreditCardProcessor(); RideService rideService1 = new RideService(creditCardProcessor); rideService1.bookRide(); // Book ride with PayPal PaymentProcessor payPalProcessor = new PayPalProcessor(); RideService rideService2 = new RideService(payPalProcessor); rideService2.bookRide(); } }

    How We Solved the Issue#

    By introducing the PaymentProcessor interface and injecting the dependency into RideService, we decoupled the high-level module (RideService) from the low-level module (CreditCardProcessor or PayPalProcessor). This adheres to DIP, making the system more flexible and easier to extend with new payment methods.

    Conclusion#

    The SOLID principles are more than just guidelines—they’re a mindset for writing clean, maintainable, and scalable code. By applying these principles in real-world applications like food delivery, ride-booking, and e-commerce platforms, you can create systems that are easier to understand, test, and extend. Each principle addresses a specific aspect of software design, and together, they form a powerful approach for building robust applications.

    Start incorporating SOLID principles into your projects today, and you’ll notice a significant improvement in your code quality. Happy coding!

    Low Level Design
    Solid Principles
    Java
    Object Oriented Programming

    More articles