SOLID Principles

SOLID-Principles-feature-image-1.jpg

SOLID principles are simple design guidelines for writing better, more maintainable software. They help programmers write code that can grow easily, is simple to fix, easy to check for errors, and clear for other people to understand. These rules originated from object-oriented programming, but now these principles are widely applied in various programming-related tasks, guiding how software components or modules should be designed. SOLID principles help you build strong software that can change easily in the future. Knowing these basic ideas is very important for anyone who wants to build good programs. In this article, we will use Java to show each rule with real examples.

Table of Contents:

What are the SOLID Principles?

The SOLID principles are the basic and foundational guidelines for designing scalable and maintainable software components, such as classes, modules, or services. Following these SOLID design principles ensures that the software architecture or logic is prepared for any future development as the project grows. These principles were first introduced by Robert C. Martin in one of his research in the 2000s, and later, Michael Feathers came up with the SOLID acronym that is now the foundation of modern applications. 

Master Java Fundamentals - Absolutely Free!
Enroll for Free and begin your journey to becoming a Java Developer today!
quiz-icon

Why Are SOLID Principles Important?

  • The primary challenge the SOLID principles deal with is tight coupling. Tight coupling is a concept where two components, functions, or classes are tightly coupled, meaning changes made to one may heavily affect the other class.
  • Secondly, these principles promote breaking down the code into smaller pieces, which reduces the complexity by dividing the problem into small, manageable pieces. 
  • The code becomes more flexible, adaptable to change, and more maintainable, since applications built with SOLID principles can adapt to changing requirements without major restructuring of the code.
  • When everyone follows the same principles, code becomes more predictable and easier for team members to understand. Not only does this promote team collaboration, but it also makes it fit for open source collaborations.
  • Boosts testability: The smaller, independent classes are easier to test in isolation, which improves the reliability of unit tests.

Understanding Each SOLID Principle

We know that each letter in SOLID stands for a particular rule. These are the Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and finally, Dependency Inversion Principle. Now, let us try to understand each of these principles in detail with examples.

1. Single Responsibility Principle (SRP)

The S in SOLID principles stands for the Single Responsibility Principle. In simple terms, this means that a software component, whether a class, function, or module, should have a single responsibility or purpose. Hence, it should have only one reason to change, or only one type of job can allow its entities (variables and functions) to change. 

Example:

Let us understand this with the help of a code example in Java, an object-oriented programming language that violates the Single Responsibility Principle.

Note: The examples provided are compatible with Java 8 and later

Bad Code:

Java

Output:

Single Responsibility Principle - Bad Example

Explanation: The problem here is that the Employee class is currently handling three distinct responsibilities: employee data management, salary calculation logic, and report generation. Therefore, you will have three reasons to change:

  • The rules for salary calculation change; you change the Employee.
  • The format of the employee report changes, and you change the Employee class.
  • If the properties of an employee (e.g., adding an address) change, you change the Employee.

This creates problems because if you change one method, it will affect the other. Also, you can’t easily reuse just the salary calculation or just the reporting part without dragging along the entire Employee class. In a non-OOP context, this could apply to a function that does only one thing well, or a microservice responsible for a single business capability.

Let us now look at how you can achieve the Single Responsibility Principle in the above code.

Good Code:

Java

Output:

Single Responsibility Principle - Good Example

Explanation: This code resolved the violation of the Single Responsibility Principle by separating the classes of report generation, employee data management, and salary calculation. Now, each class has only one reason to change.

2. Open/Closed Principle (OCP)

The O in SOLID principles stands for the Open/Closed principle. This principle states that a component or function should be open for extension but closed for modification. This principle ensures that the code is ready for future changes and accommodation while making sure that none of the new additions change the old code. It is kind of a security measure so that the old instance does not crash. If you need to add a new feature, you should extend the class rather than directly modifying its source code.

Example:

We will look at two examples in Java, one that violates the open/closed principle and the other that follows that principle, and how it makes the code better.

Bad Code:

Java

Output:

Open Closed Principle - Bad Example

Explanation: Here, the DiscountCalculator class violates the Open/Closed Principle. If you introduce a new customer type, for example, maybe a “GOLD” customer with a 20% discount, then you are forced to modify the existing calculateDiscount method by adding another “else if” block. This means the DiscountCalculator is not closed for modification when new customer types are added. Modifying existing code can introduce new bugs into previously stable and tested logic.

Good Code:

Java

Output:

Open Closed Principle - Good Example

Explanation: To not violate the OCP, we used an abstract class and relied on polymorphism. The DiscountCalculator class is now open for extension. This means you can easily add new customer types by creating new classes like GoldCustomer that extend Customer but are closed for modification since the applyDiscount method in DiscountCalculator does not need to be changed when new customer types are introduced. Now, the DiscountCalculator interacts with the abstract Customer type, instead of the specific discount logic, which is now encapsulated in each type of discount class.

3. Liskov Substitution Principle (LSP)

The L in SOLID principles stands for Liskov Substitution Principle. This principle states that objects of a superclass should be replaceable with objects of a subclass without breaking the application. In other words, subclasses should behave in a way that doesn’t surprise the users of the parent class. If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the primary properties of the program, its correctness, the task it performs, or anything else.

While LSP is most often applied in object-oriented design, similar principles apply to any system where interchangeable components follow a shared contract or behavior, such as pluggable functions or API interfaces.

Example:

Here, we will look at another code example in Java, which has a superclass Bird and various subclasses like Ostrich and Sparrow. First, we will see a bad code that violates the Liskov substitution principle, and then we will see its correct alternative.

Bad Code:

Java

Output:

Liskov Substitution Principle - Bad Example

Explanation:

This code example is a violation of the Liskov Substitution Principle. The Bird class contains a fly() method. This implies that all Bird objects can fly. However, the Ostrich subclass, which is also a Bird, overrides fly() with an UnsupportedOperationException, so an Ostrich object cannot be substituted for a Bird object without changing desirable properties of the program. Moreover, an Ostrich’s behavior is “surprising” to users who expect a Bird to fly.

Good Code:

Java

Output:

Liskov Substitution Principle - Good Example

Explanation: This code follows the Liskov Substitution Principle. We created an abstract Bird class that has a behavior defined as a generic method called move(). All birds move, but that motion is not necessarily a flight. On the other hand, the Flyable interface separately defines the fly() behavior for only those birds that are capable of flying.

  • Sparrow implements both the Bird class (through move()) and the Flyable interface (through fly()) because it also flies.
  • Ostrich behaves differently from other birds in terms of how they move. Instead of forcing Ostrich to fly like the other birds, the system makes sure that every object still implements the move() functionality correctly (no matter what that looks like).

So, now, when you use a Bird object, you can safely call move() without any surprises because each subclass implements move(). When you are explicitly looking for a bird that can fly, you program to the Flyable interface, ensuring that any object passed in as Flyable has a working fly() method. This attempt at preventing surprises at runtime by understanding the set behavior shows the Liskov Substitution Principle.

Get 100% Hike!

Master Most in Demand Skills Now!

4. Interface Segregation Principle (ISP)

The I in SOLID principles stands for the Interface Segregation Principle. This principle asks the developers to create a module or API that should not force consumers to depend on methods they don’t use. This way, clients won’t be forced to waste resources on components that are irrelevant to their purpose. The code will also become cleaner and maintainable, and will not use unnecessary code.

In RESTful APIs or microservices, a client should not have to interact with endpoints irrelevant to its needs.

Example:

Let us illustrate this with an example where we will use a large interface and then a small interface.

Bad Code:

Java

Output:

Interface Segregation Principle - Bad Example

Explanation: This code violates the Interface Segregation Principle. The ISPBadWorker interface is forcing both the ISPBadHumanWorker class and the ISPBadRobotWorker class to implement all three methods (work, eat, sleep). While a human worker can perform all these actions, a robot worker cannot eat or sleep. This forces the ISPBadRobotWorker to provide a dummy implementation (throwing UnsupportedOperationException), which is a clear sign of an ISP violation. Classes (or clients) that use ISPBadWorker are forced to depend on methods they don’t use or that aren’t applicable.

Good Code:

Java

Output:

Interface Segregation Principle - Good Example

Explanation: This class implementation follows the Interface Segregation Principle. Instead of one large interface, we have separated the ISPBadWorker into three smaller, more specific interfaces: ISPGoodWorkable, ISPGoodEatable, and ISPGoodSleepable.

  • ISPGoodHumanWorker implements all three of the relevant interfaces (ISPGoodWorkable, ISPGoodEatable, ISPGoodSleepable), because it can perform all those actions.
  • ISPGoodRobotWorker only implements ISPGoodWorkable because it can only work.

Clients of these workers can depend only on the interfaces that are relevant to them. A part of the system that only needs workers to work can take ISPGoodWorkable objects, does not have to care whether they eat or sleep. This allows for a more elegant, flexible, and reusable design of a module or component.

5. Dependency Inversion Principle (DIP)

The D in SOLID principles stands for the Dependency Inversion Principle. This principle encourages separating high-level processes from low-level implementations. Whether in code modules, services, or system layers, this principle helps keep the main logic insulated from specific implementation details. Instead of connecting them directly to each other, you should connect them to a middle layer (abstract class or interface). This way, if you change the small details (like switching from one database to another), the important parts of your code won’t break. It makes your code easier to maintain, test, and reuse.

Example:

Bad Code:

Java

Output:

Dependency Inversion Principle - Bad Example Output

Explanation: This is an example of tight coupling that also violates the Dependency Inversion Principle. The UserService, which is a high-level module representing business logic, directly depends on the MySQLDatabase, which is a low-level module representing a specific implementation detail.

  • The high-level depends on the low-level: As you can see here, the UserService is tightly coupled to a MySQL database. If you wanted to switch to a PostgreSQL database, you would have to change the UserService class.
  • Abstractions depend on details: There is no abstraction layer implemented between the UserService and MySQLDatabase. The high-level logic is dependent on the implementation of a specific database.

This tight coupling adds inflexibility to the code, makes it harder to test (as you won’t be able to easily swap out the database with a mock for your tests), and makes it more difficult to maintain if the database technologies change.

Good Code:

Java

Output:

Explanation: This code adheres to the Dependency Inversion Principle (DIP) in the following ways:

  • The high-level module depends on an interface: The DIPUserService class uses the DIPDatabase interface and does not deal directly with a specific database.
  • The low-level modules also depend on the interface: Classes implementing the DIPDatabase interface, like DIPMySQLDatabase and DIPPostgreSQLDatabase.

This allows the application to declutter business logic in regards to the database. The code will be relatively simple to alter and test, and will allow for new management to update the database whenever they want without needing to change the main app at all.

Common Mistakes While Implementing SOLID Principles

  • Developers often try to apply all five principles all at once or are unable to apply them to the specific needs of the project, resulting in overengineering and needless complexity.
  • An example of a module or component violating the Single Responsibility Principle would be a REST API, such as user authentication and data analytics. The API is performing unrelated services, violating SRP.
  • Overusing abstraction will sometimes lead to the use of an excessive amount of interfaces or abstract classes, making code harder to understand and maintain, as well as structurally overcomplicated without regard to use cases.
  • Regarding substitution rules, overuse of inheritance, instead of composition, can lead to tight coupling and ultimately less flexibility.
  • In the hopes of working on the design without proper testing, developers sometimes overlook the testing step to save time. This negates one of the key benefits of SOLID, which is ease of testing and debugging.

Real-World Applications of SOLID Principles

  • In e-commerce platforms, modules like product listing, cart, and payment follow the Single Responsibility Principle, keeping each function separate and manageable.
  • Banking and financial systems use interfaces to connect with different payment gateways, applying the Dependency Inversion Principle for flexibility and easy integration.
  • Systems like CMS platforms or mobile apps apply the Open/Closed Principle by enabling new features or plugins through configuration or modular integration, without altering the existing core behavior.
  • In game development, different object types implement only the required actions, following the Interface Segregation Principle for cleaner, more efficient design.
  • IoT systems and microservices benefit from SOLID by keeping components loosely coupled and focused, improving scalability, testability, and long-term maintenance.

Conclusion

The SOLID principles are fundamental principles that software developers can use to develop systems that they can maintain, test, and extend more easily over time. While SOLID principles can be applied across programming paradigms, you can consider the principles in a variety of contexts, from writing modular code, writing APIs, and designing services, to name a few. I encourage you to start applying one principle at a time in your real-world experience. With practice, you will start to see where you get the most value from each principle. Whether you are writing in Java, Python, JavaScript, designing microservices, or REST APIs, implementing SOLID will ultimately lead to you writing cleaner and more flexible software, and you will develop as a well-rounded developer.

SOLID Principles – FAQs

Q1. What is the SOLID principle?

You can see SOLID as a set of five design guidelines that help you write clean, maintainable, and scalable object-oriented code.

Q2. What are the 5 rules of SOLID?

You should know the five rules: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles.

Q3. What is the difference between SOLID and DRY principles?

You can view SOLID as design principles for structure, while DRY (Don’t Repeat Yourself) focuses on reducing code duplication.

Q4. Are SOLID principles only for OOP?

You’ll mostly apply SOLID to object-oriented programming, but some principles can inspire good design in other programming paradigms too.

Q5. Why might some developers avoid using SOLID principles?

You may find SOLID complex initially or feel it over-engineers simple projects, so balance its use based on project size and needs.

About the Author

Senior Consultant Analytics & Data Science, Eli Lilly and Company

Sahil Mattoo, a Senior Software Engineer at Eli Lilly and Company, is an accomplished professional with 14 years of experience in languages such as Java, Python, and JavaScript. Sahil has a strong foundation in system architecture, database management, and API integration. 

fullstack