Microservices Architecture, one of the more challenging aspects
Momen Negm
CTO | Engineering Manager | Software Solution Architect | FinTech | AI | Digital Transformation
In Microservice Architecture, each microservice is characterized as a distinct, deployable software unit with a focused purpose, designed to perform its function effectively. A key benefit of this architecture is Team Autonomy. This allows the team responsible for a particular microservice to independently determine its features, release schedule, and other aspects, without the need for extensive coordination with other teams.
They must ensure that the microservice's independent deployment should still be maintained.
Teams often face a dilemma in determining the appropriate size of a microservice. If the service is too finely grained, it can result in excessive communication between services, causing coupling and performance problems. Conversely, if the service is too coarsely grained, it may mirror the challenges encountered in a Monolithic Architecture, negating the benefits of adopting a microservices approach.
Determining the appropriate size of a microservice involves a nuanced decision-making process. The question arises: should the 'micro' aspect of a microservice be defined by the number of lines of code, or by the quantity of classes and interfaces it contains? There's no definitive answer to this, highlighting the complexity of deciding a service's granularity. How does one choose the right scale for a service when traditional metrics like code length or class count don't provide clear guidance? This decision is often more art than science, requiring a balanced consideration of various factors unique to each project.
Deliberating within a business context often clarifies concepts, so I intend to apply a familiar business scenario to provoke thought. The situation involves a team grappling with a decision: Should they opt for a single composite microservice or a combination of several smaller microservices? This dilemma is a common one in business environments, requiring teams to weigh the pros and cons of each approach carefully.
The Notification Service, designed to send messages via Email, SMS, and In-App Notifications, is a common feature in numerous applications. When tasked with implementing this service, a team typically proposes two distinct solutions.
1- A multifaceted, coarse-grained Notification microservice designed to manage all types of notification channels.
2- Several distinct, finely-tuned microservices, each dedicated to a specific type of notification channel.
Logically, the purpose of the Notification service is to send messages to users. This function appears to be very cohesive, suggesting that a single, larger-grained microservice would be adequate. This approach aligns with the Single Responsibility Principle, where one microservice is responsible for this specific, well-defined task.
Conversely, due to the necessity of dispatching messages across various channels or integrations, each with its unique set of challenges, the rationale for several finely-grained microservices, each dedicated to a specific type of integration, is also apparent.
This scenario presents a complex decision-making process: should one opt for a singular, composite microservice or a collection of smaller, more specialized ones?
Fortunately, there are strategic methods to guide this decision. Below are some key factors that can assist in determining the appropriate granularity of the service:
Driver #1 — Service Functionality
A microservice should ideally provide a single, well-defined function and exhibit high cohesion. While this principle is straightforward in many instances, it can become complex in certain scenarios, such as with the notification requirements described earlier.
Deciding whether to use one service or multiple services for different types of notifications can be a grey area in implementing these requirements. This decision hinges on how one interprets the concepts of 'Single Purpose' functionality and 'Cohesion.' In some cases, what constitutes a Single Purpose function can be subjective and open to interpretation.
领英推荐
A typical example from a Food Delivery Platform where decision-making is relatively straightforward due to low cohesion might involve scenarios like
A platform user with a profile can set preferences for delivery notifications and food recommendations. Additionally, after food delivery, they can give feedback on their experience and the food's quality. Although these functionalities center around the User, they differ significantly and exhibit low cohesion. Consequently, it's practical to divide these requirements into distinct services such as a Feedback Service, User Service, and possibly a Preference Service. Each of these would adhere to the Single Responsibility Principle, ensuring high cohesion within their respective functionalities.
Driver #2 — Scalability
In a Microservices Architecture, distinct features often require varying levels of scaling. This can lead to a situation where dividing a larger microservice into several smaller ones becomes beneficial for cost efficiency and enhancing user experience.
Take the example of a Notification system - if the demand for Email notifications is 10 times higher than that for SMS, and In-App notifications experience a load 3 to 5 times that of SMS, using a single, combined service for all these functions could adversely affect the Mean Time to Start (MTTS). This is because scaling up to accommodate higher loads would require launching more instances of the service, necessitating greater computational power and memory, thereby increasing costs.
Therefore, it wouldn't be practical to scale up all services just to meet the high demand for Email notifications. It's more efficient to have separate, dedicated services for each type of notification delivery. Individual scalability for each feature is a crucial aspect of this approach.
Driver #3— Fault Tolerance
In Microservices Architecture, it's crucial for the system to be fault-tolerant. This means that if one service, such as Email in a Notification system, fails, the other services should continue functioning normally. For instance, if the Email service encounters a bug leading to a crash and it's part of a single, monolithic Notification service, this would negatively impact all types of notifications, demonstrating low fault tolerance.
To achieve higher fault tolerance, it's necessary to segment the Notification service into multiple, independent services. This way, a failure in one specific function, like Email, won't disrupt the entire notification system, maintaining the system's overall reliability and resilience.
Driver #4 — Code Volatility
A major limitation of Monolithic Architecture is that any small modification necessitates retesting the entire application. This expands the testing scope and duration, and can affect system availability, especially if deployment requires downtime.
When alterations are frequently made in a specific part of the application, it's practical to separate that functionality and manage it as an independent microservice. This approach narrows the testing range, saves time, and enhances the application's stability.
For instance, in a notification system, if the In-App Notification feature undergoes frequent updates compared to other notification types, it would be beneficial to treat Email Notification as a separate service. The remaining notification types could then be handled by another unified microservice.
Driver #5 — Extensibility
Extensibility is the capacity to enhance a service with additional features without compromising its current availability and reliability.
Consider the scenario of a Notification service initially designed as a unified system. If future expansions are planned, such as adding Browser Push Notifications and Native Mobile Push Notifications, modifications to this consolidated service would require a broader scope of testing. Additionally, depending on the deployment strategy employed, these changes could introduce risks during deployment.
So, if we know in advance that the service will have to support more notification channels in future, then breaking the service into multiple services will be helpful.
These decision drivers are equally applicable for situations where you are creating a new microservice or thinking of breaking an existing monolithic application into microservices.