Microservices are getting widespread adoption in software development. It is imperative to have a list of design principles to help developers implement the microservice architecture accurately.
Similar to how SOLID helped Object-Oriented programming have a reference list of design principles, we now have IDEALS to support the developers in building microservice-based applications.
Some of the principles of SOLID are included in IDEALS too. But IDEALS also incorporates principles that are specific to microservice-based architecture.
Having a clear understanding of the usage and drawbacks of various patterns and principles can help you build a stable and maintainable microservice-based application.
Let's dive deep into each of these principles to understand them better.
Interface Segregation
The microservice architectural style is a technological evolution of the good old service-oriented architecture(SOA). In SOA, one of the essential factors is the design of service interfaces or contracts.
Usually, the SOA suggests defining a single contract and letting every service client adhere to it. The contract gets bloated eventually as it needs to accommodate varying requirements from its distinct consumers.
Thus the efficiency and maintainability of the service reduce over time.
The interface segregation principle suggests each microservice interface should attend to a specific need of a client's program. In other words, clients should not be forced to depend on an interface they don't use.
One way to implement the principle is through an API gateway. A gateway helps to understand various communication protocols, translate them as needed, modify the request and response to suit the service and the client programs, etc.
Deployability (Is on You)
Microservice architecture increases the distributed nature of the applications by multifold. Now teams have a relatively large number of modules, with each running on several nodes dynamically maintained by their cloud platforms.
In this highly distributed environment, deployment is no longer an eventuality for software development. Continuous deployment has become as important as any other phase in the lifecycle.
Deployability, as a requirement, cannot be addressed “later” anymore. — Jacek Chmiel
Organizations are focusing on automating their entire deployment process from the dev environment to the production. The success of the development teams is now measured in how fast and effortlessly they can deploy quality products for their business.
With near-zero downtime becoming the de facto standard for applications, teams must plan their deployment methodology as early as in the application design phases.
They should also have solid plans around monitoring and traffic management in their production environment. All these efforts combined can help the teams maintain their application's deployability to a great extent.
Here is an excellent definition of what does it mean by deployability:
Configuring the runtime infrastructure, which includes containers, pods, clusters, persistence, security, and networking.
Scaling microservices in and out or migrating them from one runtime environment to another.
Expediting the commit, build, test, and deploy process.
Minimizing downtime for replacing the current version.
Synchronizing version changes of related software.
Monitoring the health of the microservices to identify and remedy faults quickly.
Event-Driven
In an event-driven architecture, the communication between applications generally happens over message queues (Kafka Topics, RabbitMQ, etc.). The source application creates a message based on an event in its domain that somehow manipulates the data.
One or more applications (services), called consumers, listen to the message queue to get information about the event. Consumers are free to decide their business logic depending on the event and the message data.
One of the critical design decisions to make microservices fault-tolerant is to avoid cascading failures caused by downtimes of dependent services. Event-driven architecture helps in this aspect as the producer and consumers of the event are not tightly coupled.
The messaging channel used for communication keeps both ends of it independent and hence keeps them loosely coupled.
The producer does not wait for the consumer(s) to respond. Simultaneously, the consumer does not go down because of the producer's unavailability.
Overall, event-driven architecture helps in improving the reliability, scalability, and throughput of the entire system.
Event-driven microservices do have specific challenges that developers need to be mindful of to create a stable system. Due to the asynchronous nature of the communication, it requires extra effort to correlate all the actions performed end to end for a single event.
Developers need to take care of handling lost messages due to message channel failures. There needs to be an additional consideration for handling out-of-sync messages, partial message deliveries, and reverting data state due to unforeseen circumstances.
Availability Over Consistency
Every microservice design has to answer whether to have the right data or to have the data right now. There is no correct answer to it. The strategy depends on what kind of data the service is dealing with and how its availability impacts user behavior.
You can read about the topic of eventual consistency in detail in Pat Helland's article 'Mind Your State for Your State of Mind'.
Most of today's eCommerce applications prefer to load their pages faster, even if they might not have accurate data. Research suggests users drop off the site if the cart, products, or reviews do not load fast enough.
It is alright to show a product to the user even if it is out of stock. Users can add it to the cart, and a second validation during the checkout can inform the product's availability. This is essential for the sites to retain the customer.
Companies do not want users to go to another site while their page is slow to load; because the application intends to verify stock in inventory and other carts to give an accurate product availability status.
It means the availability of the data is more critical in this case than its accuracy.
At the same time, there are applications used for patients' medical details or used in nuclear power plants that do not have the luxury of eventual consistency. The data must be accurate and consistent, even if that means slower performance.
Hence, while designing your microservice-based application, you need to decide the priority between availability and consistency.
Message replication through event-driven architecture generally works on eventual consistency. There can be lags due to queue backlogs or a slow-processing consumer. But the data eventually gets processed by the client application and becomes consistent with the source data.
However, reading directly from the source system through APIs is the preferable option for applications that need consistently accurate data.
Loose-Coupling
Often, services need to communicate with each other to complete an entire workflow. Thus, they can become tightly coupled. A tightly coupled system makes it difficult to introduce changes in one service without impacting the other services.
Also, in tightly coupled services, there is a chance of a high blast radius when one of the services in the chain fails. This makes the reliability of the entire system less predictable.
It is advisable to keep them as loosely coupled with the service internals as possible when it comes to service interfaces.
Keeping the interface contract independent of the business logic or technology will help the teams evolve with time without necessitating a change on the service's consumer side.
To make the services loosely coupled, you can follow a publisher-subscriber model. In this model, the source and consumers are detached from each other through a messaging channel.
Thus, downtime for one of the applications does not impact other applications in the ecosystem.
Also, consumers are not impacted when the producer's internal logic changes as long as the message format and definition do not change.
API gateways can also help to keep the services loosely coupled. It provides an intermediary component that can handle the required translations between service providers and their consumers.
Gateways can handle protocol bridging, message format conversion, routing, etc., so consumers and providers do not have to stick to a single definition. It also abstracts any change in the service layer from its consumers.
That, in turn, helps the entities to stay loosely coupled.
In both publisher-subscriber and API gateway examples, we can change or add new entities (publisher/client app, subscriber/service) without any change required on the rest of the entities in the ecosystem.
Single Responsibility
The Single Responsibility Principle in SOLID proposes high cohesion among the functionalities of a class.
The idea is to make the class responsible for one thing and just one thing. The alternate way SRP can be defined as— a class should have only one reason to change.
When designing our classes, we should aim to put related features together, so whenever they change, they change for the same reason. And we should try to separate features if they will change for different reasons. — Steve Fenton
This, in turn, helps in writing easy-to-understand code that is highly cohesive and easier to evolve with changes in business requirements.
Extending the SOLID's SRP to microservices, you should strive to make your microservice serve just one functionality.
If you plan to package multiple microservices together into a single deployment, they all should attend to one single domain of work with a relatively similar cycle for change.
At the same time, you may not want to define your microservices at a too fine-grained level. If a feature needs other features to change together almost all the time, separating them will increase the maintenance burden without much advantage.
An axis of change is only an axis of change if the changes actually occur. It is not wise to apply the SRP, or any other principle for that matter if there is no symptom. — SRP: Single Responsibility Principle
As such, distributed applications already require high maintenance than monoliths. It will be challenging for the teams to limit interdependency with too fine-grained services, leading to a maintenance nightmare.
Final Thoughts: IDEALS Microservices Principles
Each of the design principles discussed above has its trade-offs, like any design decision we make in software development. Hence, it is not to be followed blindly.
As microservice-based applications are continually a work in progress, you can use your own experience, your company's need, and the above six principles in your design decisions to help you create a robust microservice architecture.
I would like to hear about your experience with microservice-based application development. Please feel free to share your feedback, suggestions, or opinions in the comments.
Subscribe to my free newsletter to get stories delivered directly to your mailbox.
Nice article.