The SOLID principles are five essential guidelines that enhance software design, making code more maintainable and scalable.
Main aim of SOLID principles -
To make software easy to change without breaking existing behavior
When to use?
SOLID principles should be kept in mind everywhere while designing software. They are meant to be be applied to modules, aiming to keep software soft, not rigid. They should be treated as lighthouses guiding the developer in the right direction even if we cannot see the future.
Similar to how Math helps us think in higher dimensions, SOLID principles help us think ahead of the code being written. It is very easy to write code that works. But, writing maintainable and resilient code is what most developers miss out on.
SOLID principles should inform our decisions, they are not meant to be followed blindly. It is very easy to over engineer and over complicate your code. The main aim should be always kept in mind and questioned at every decision.
It is best practice to create a prototype as fast as possible and get feedback from the customer, so that you can predict the axis of change in future and design your abstractions accordingly
The 5 principles
1. Single Responsibility Principle
Single Responsibility Principle (SRP) states that a module should have one reason to change, meaning it should be responsible to one actor, or to multiple actors whose requirements always change together.
It is essential for identifying where module boundaries should exist so that changes driven by one actor do not impact unrelated parts of the system (Don’t mix concerns).
SRP decides where change belongs. It isolates change by promoting high cohesion, meaning, the methods and properties within the class are closely related, making the module easier to maintain.
For example, the system is broken down into different layers -
- Controller layer - responsible to the API contracts
- Service layer - responsible to the business logic
- Repository layer - responsible to the persistence logic
Another way SRP can be applied -
- User service - responsible to product/personalization teams
- Playback service - responsible to streaming/media teams
- Billing service - responsible to financial/legal decisions
2. Open Closed Principle
Open Closed Principle (OCP) states that a module should be Open for extension but Closed for modification, meaning, new behavior should be introduced by adding new code instead of modifying existing stable code.
OCP decides how change is added by enabling safe growth. Modification of stable code increases the risk of regressions and unintended side effects. OCP is commonly implemented through the help of interfaces and inheritance (Polymorphism).
OCP isolates change by extension - by allowing you to add new functionality without changing the existing, tested code.
For example, implementing separate connector services to connect with different CBS is exercising OCP, since the existing implementations are not affected to add new functionality.
In case of bugs, modification has to be done to achieve the original intended purpose
3. Liskov Substitution Principle
Liskov Substitution Principle (LSP) states that objects of a superclass must be replaceable by objects of it's subclasses without breaking the application
LSP decides whether change is safe. It constrains and validates Open Closed Principle (OCP) by ensuring that new implementations introduced through extension are behaviorally substitutable. In other words, OCP enables adding new code, while LSP ensures that this extension does not break existing behavior.
LSP also validates the inheritance structure by ensuring that child implementations truly belong to the hierarchy in a behavioral sense. Violations are commonly resolved by applying ISP, which prevents forcing implementations to support incompatible behavior.
In practice, LSP becomes especially important when using dependency injection, where implementations are substituted at runtime. For example, substituting different CBS connector implementations via dependency injection should not break the application or violate expected contracts.

Changes to avoid while implementing/inheriting an interface/class -
- Introducing stricter preconditions or weaker postconditions
- Changing the return type in an incompatible way
- Altering the semantic meaning of existing behavior
- Throwing new or broader unchecked exceptions that callers are not prepared to handle
4. Interface Segregation Principle
Interface Segregation Principle (ISP) states that implementors of an interface should not be forced to implement methods they do not use.
ISP encourages splitting large, bloated interfaces into smaller, more specific interfaces so that the implementing classes only concern themselves with relevant methods.
ISP can also be applied at a high level in system architecture, module design and API design. Clients should only be exposed to the needed functionality. Systems become easier to understand, test, and maintain when dependencies are specific and clear.
5. Dependency Inversion Principle
Dependency Inversion Principle (DIP) states that high level modules should not depend on low level modules directly; both should depend on abstractions
DIP introduces an abstraction layer between the high and low level modules. The abstractions introduced should not depend on details. The abstraction handles the contract part and the high and low level modules have to follow the contracts.
Without an explicit abstraction, the low-level module owns the contract, so it is free to evolve its API and semantics in ways that can silently break high-level modules (The low level module is not aware of the high-level module and is free to change).
Dependency Inversion Principle (DIP) is most commonly implemented using interfaces or abstract classes, where the high-level and low-level modules depend on an abstraction rather than a concrete implementation.
DIP isolates change by decoupling the high level modules with the low level modules.
In case there is a change in the abstraction, both the low level and high module will have to adapt. But, the low level module cannot change itself since it is now aware of the abstraction layer and has to adhere to the contract. The dependency is now inverted from high -> low to high -> abstraction <- low.
This inversion enables implementations to be swapped without affecting high-level logic. Such extensibility is governed by the Open Closed Principle (OCP), which allows new implementations to be added without modifying existing code, and validated by the Liskov Substitution Principle (LSP), which ensures that substituted implementations remain behaviorally compatible.
Note
SRP and DIP are primarily about creating the structure of a system by defining clear responsibilities and dependency boundaries, while OCP and LSP are about evolving that structure safely over time. SRP and DIP enable OCP and LSP by establishing stable boundaries and contracts. In non-trivial systems, safe and reliable extension is difficult without first putting the right structure in place.