Understanding microservices design patterns
To fully realize the benefits of any architecture (including the microservices architecture), an architectural approach is often backed with design patterns. Understanding these design patterns is crucial for an ideal adoption of the architecture. In the following sections, we will cover some practical and commonly used design patterns in microservices. Each pattern addresses a different aspect of the application development life cycle and our focus would be to see these design patterns from a practical usage standpoint. We will begin with decomposition design patterns.
Decomposition design patterns
Decomposition design patterns dictate how we can componentize or decompose a big/monolithic application into smaller (micro) services. These patterns come in handy in designing a transformational architecture for any legacy monolithic application. The following are the most commonly used design patterns in decompositions.
Decomposing by business capability
Any business capability is an instrument to make a profit. If we can enlist and categorize an application into a set of business capabilities such as inventory management, customer orders, or operations, then the application can be decomposed into microservices that are based on these business capabilities. This process to decompose is effective and recommended for small- to medium-sized applications.
Decomposing by domains/sub-domains
If the application is an enterprise-grade and heavy application, then the previous approach may end up decomposing the application into smaller monoliths. These monoliths are smaller but monoliths nonetheless. In such cases, business modeling can help to categorize and map application functionalities into domains and sub-domains. Functionalities inside a domain/sub-domain are similar but very different from the functionalities of other domains/sub-domains. Microservices then can be designed and built around domains or sub-domains (if there are many functionalities mapped to a domain).
Integration design patterns
Once the application is broken down into smaller (micro) services, we will need to establish cohesion among these services. Integration design patterns address such collaboration requirements. The following are the most commonly used design patterns in integrations.
The API gateway pattern
Often upstream frontend consumers need to access microservices through a façade. This façade is called an API gateway. The API gateway design pattern serves an important purpose to keep things simple for frontend clients:
- The frontend client is not sending too many requests to microservices.
- The frontend client is not processing/aggregating too many responses (from microservices).
- At the server end, the gateway routes a request to multiple microservices, and these microservices can run in parallel.
- Before sending the final response, we can aggregate individual responses from different microservices.
The aggregator pattern
This pattern is very similar to the aforementioned API gateway pattern. However, composite microservice is the key differential. The mandate of a composite microservice is to offload an incoming request to multiple microservices and then collaborate to create a unified response. This pattern is used when a user request is atomic from business logic standpoints, but it is processed by multiple microservices.
The chained microservices pattern
In some scenarios, an incoming request is executed in a series of steps wherein each step could be spinning off a call to a microservice. For example, ordering an item in an online marketplace would require the following:
- Searching for an item (inventory management service)
- Adding an item to the cart (cart service)
- Checking out the added item (payment service, mail service, inventory management service)
All these service calls would be synchronous. Fulfilling a user request would be an amalgamation of all these chained microservice calls.
Data management patterns
Integrating with the persistence layer is an important aspect of any microservice-based application. Greenfield (net new) and brownfield (legacy transformation) applications may dictate their requirements in how to choose a data management pattern. The following are the most often used design patterns in data management in microservices.
Database per service
In greenfield (net new) applications, it is ideal to have a database per service. Each service is the owner of an isolated database (relational or non-relational) and any data operation must be executed through the microservice only. Furthermore, even if any other microservice needs to perform a database operation, then it should be routed through the owner microservice.
Shared database
In brownfield (transformational) applications, it may not be practical to decompose the database into one database per service. In such scenarios, the microservices architecture realization can be kickstarted with services sharing a common monolith database.
Command query responsibility segregation (CQRS)
In greenfield or fully transformed applications where each microservice is an independent database owner, there might be a requirement to query data from multiple databases. The CQRS pattern stipulates to decompose an application into a command and query:
- Command: This part will manage any create, update, and delete requests.
- Query: This part will manage query requests using database views where database views can unify data from multiple schemas or data sources.
Cross-cutting patterns
Some concerns cut across all the different aspects/layers of microservices. In the following sub-sections, we will discuss some of these concerns and patterns.
The service discovery pattern
In a microservices-based application, each microservice may have more than one instance at runtime. Furthermore, these service instances can be added or removed at runtime based on traffic. This runtime agility can be an issue for upstream consumers in how they connect with services.
The service discovery pattern addresses this by implementing a service registry database. The service registry is a metadata store containing information such as the service name, where the service is running, and the current status of the service. Any change to the service runtime information will be updated in the service registry, for example, when a service adds a new instance or a service is down. This eases the pain for upstream consumers to connect with different microservices in the application.
The circuit breaker pattern
In a microservices-based application, often services interact with each other by invoking endpoints. There could be a scenario where a service is calling a downstream service but the downstream service is down. Without a circuit breaker, the upstream service will keep calling the downstream service while it's down and this will keep impacting the user interaction with the application.
In the circuit breaker pattern, a downstream service call will be routed through a proxy. This proxy will timeout for a fixed interval if the downstream service is down. After the timeout expiry, the proxy will try to connect again. If successful, it will connect with the downstream service; otherwise, it will renew the timeout period. Therefore, the circuit breaker will not bombard the downstream service with unnecessary calls and it will not impact user interaction with the application.
The log aggregation pattern
In the microservices landscape, often an incoming request will be processed by multiple services. Each service may create and log its entries. To trace any issues, it will be counter-intuitive to access these sporadic logs. By implementing a log aggregation pattern, logs could be indexed in a central place, thereby enabling easy access to all application logs. Elasticsearch, Logstash, Kibana (ELK) can be used to implement log aggregation.
In this section, we covered some often-used design patterns in different stages of the application life cycle. Understanding these design patterns is required to fully reap the benefits of microservices architecture. In the next section, we will dive into the Micronaut framework for developing microservices.