Microservices: Designing Effective Microservices by following SOLID Design Principles

Saurabh Gupta
7 min readMay 24, 2022

--

A Microservice is not just breaking a big monolithic application into sub-applications, it’s a lot more than that. The concept and epicenter of Microservices revolve around creating a self-contained piece of functionality that offers clear interfaces and could have its own internal components. For more details on microservices and their communication.

Good and Effective Microservices design is sometimes a pain point for many architects and developers. In this article, I’ll provide a walk-through on the basics of the SOLID principles and how these principles can be applied to Microservice architecture in order to make our life easier.

SOLID is a set of 5 design principles meant for service development and implementation. Most developers (I would say Good ones) use them as guidelines to implement business services with Object-Oriented languages such as Java. The promise of SOLIDly designed services is to provide good quality through more efficiency, maintainability, dependability and Accessibility.

SOLID Design Principles

Single Responsibility Principle

Open/Closed

Liskov substitution

Interface Segregation

Dependency Inversion

Single Responsibility Principle

The single responsibility principle states that a class should have only a single purpose and a single reason to change throughout its lifetime. This principle ensures that a class exists only for one specialized reason but can have multiple methods to carry out different functions.

If our code follows this one, it’s easier to be followed, understood, debugged, removed, and refactored. If we have to change things (enhancements) in microservice, it should not break anything or the whole system. Following this principle is a key to easier changes. In the worst case, if we break something, we break one thing (or fewer), and not an entire system.

Microservices should be modeled in a similar fashion. Building stuffed/over-engineered services which are subjected to change for more than one business context is a bad practice and will eventually lead to highly coupled code.

If an application has multiple use cases or business functions, each microservice should implement only one business function/feature. And, each microservice should be modular enough to be reused in multiple use cases. Ideally, microservice should not specify any processing logic that is too complex (e.g. a single microservice should not take an online shopping order, process the payment and check order status). This calls out a need for an orchestrator service/utility to call each service in the correct order. An orchestrator can be an API Gateway or Control-M or Airflow, etc. I would suggest using a API Gateway for the APIs and for other functions Airflow is good, especially with the scheduling the services (or step-functions).

Open/Closed Principle

“Objects should be open for extension (and improvements) only and not for modification”. The open/close principle ensures our code is easily extensible and does not require editing or rewriting the existing codebase.

A microservice should never have to be changed to provide situational or tactical fixes and functionalities. This can be achieved by following a “Program by Interface, not by Implementation” approach. Moreover, the OCP can also be achieved via “Decorator Design Pattern”. Decorator pattern allows a user to add new functionality to an existing object without altering its structure.

Open/Closed Principle

Liskov substitution Principle

This principle illustrates that if a section of our code is extending a superclass, then all subclasses of this superclass should be able to replace the superclass in the code. A section of our code does not have to know which class it is as far as it is a superclass subclass.

The context is about many objects which can be easily replaced by objects of the same nature. This principle benefits the fast integration of new objects

Versioning the microservices is a good way to support the Liskov principle. A new version of a microservice should always be fully backward compatible with the previous version.

This looks simple but it's very difficult to maintain. Even upgrading to new versions of Dependencies/libraries leads to the breaking of the existing functionality

Liskov substitution

Interface Segregation Principle (ISP)

This principle advocates for less coupling in code plus clean and clear code and functionalities. It states that no code should be forced to depend on methods/functions/snippets it does not use. The interfaces should be more specific to the methods it’s going to support. It discourages creating blanket interfaces. The small interfaces that are specific to the features its methods support are called role interfaces and such interfaces are easier to change, refactor and deploy/redeploy.

As per ISP, a microservice should not expose non-related methods/functions (like inventory and Ordering processing systems, User Profiles and User Orders). If we integrate with a microservice for a specific use case and we only use less than 30–40% of its features, then we are probably not calling an unorganized small-monolithic services which is not a microservice.

Dependency Inversion Principle

The dependency inversion principle also advocates and states a specific methodology for loosely coupling software modules. As per Wikipedia, the principle states:

  1. High-level modules should not consume or import anything from the low-level modules. Ideally, both High-level modules & low-level modules should depend on abstractions (or interfaces).
  2. Abstractions should not depend on details/concrete implementations. Instead, concrete implementations should depend on abstractions.

In simple words, the interactions between high-level and low-level modules should be through abstract interactions between them. here high-level can be interfaces/abstract classes and low-level refers to concrete functions/methods.

A microservice should not directly call another microservice. There are two ways to achieve inter-service communication- Synchronous & Asynchronous.

Synchronous(HTTP requests) : Synchronous communication means that two or more services exchange information in real-time. As per DIP, there should not be a direct communication between two microservices. The good way is to use either a Service Locator or Discovery Feature, to locate a microservice instance and then delegate the execution to the runtime platform, like, with a message queue in the middle

Dependency inversion principle

Asynchronous: Asynchronous communication refers to the exchange of data/information/messages between two or more services without the requirement for all the services to respond immediately. In simple words the interacting services are not required to be up and running during communication. This can be achieved via either Messaging queues or Database polling.

Benefits of SOLID designed services:

1. Ease of refactoring (or maintainability) Maintainability is the capacity for a system to be resilient even after being changed and to easily support new features/functionalities.

If we cannot add a new feature without breaking the system, it is not maintainable.

2. Efficiency is its capacity to be as performant as possible by using an optimized amount of resources.

If your application is just less than 30% of its runtime environment capabilities, the overall system can be optimized (e.g. downscaling, splitting application…).

3. Dependability relates to its levels of availability, security and overall reliability.

If we cannot rely on predictable outputs from given inputs to the system, we cannot depend on it.

4. Usability is all about having an understandable interface.

If we have to read a full manual to know what kind of service provides the system, it might not be that usable.

5. Readability A well-designed codebase can be easy to understand and read.

Readability is also an integral element in software development as it makes the debugging and refactoring operations easier.

Microservices guidelines and SOLID principles

As there is no concrete definition or rules of what microservices actually are but only guidelines, it would be nice to check whether or not a SOLIDly designed API would follow those.

As per microservices.io, the guidelines are the following:

  • Highly maintainable
  • Testable
  • Loosely coupled
  • Independently deployable
  • Organized around business capabilities
  • Owned by a small team

We can achieve all the above guidelines via SOLID principles. Below is the mapping:

MIcroservice Guidelines & SOLID mapping

All guidelines are covered!!

Conclusion

To sum up, I feel that services and APIs designed following the SOLID principles are actually good microservices. I would prefer saying “SOLID principles can be taken as an integral reference for designing effective microservices.”

Thanks for the read. I hope you liked the article!! As always, please reach out for any questions/comments/feedback.

LinkedIn: saurabh-gupta-engr
Github:
SaurabhGupta-repo

Reference:
https://microservices.io/
https://en.wikipedia.org/wiki/Interface_segregation_principle
https://en.wikipedia.org/wiki/Dependency_inversion_principle

--

--