In this section, we will explore how to effectively perform queries across microservice and discover the patterns and practices that should be applied in such scenarios.
In traditional monolithic architectures, querying different entities is straightforward as they are all stored in a single database. Data management and querying across multiple tables is relatively simple. Any updates or changes to the data are applied together or rolled back as a whole. Relational databases with strict consistency provide ACID transactions, making data management and querying easier.
However, in microservices architectures, where each microservice has its own databases with a mix of relational and NoSQL databases (known as polyglot persistence), a strategy must be established to manage data interactions during user interactions. This requires adopting various patterns and practices to handle data integration between microservices.
Microservice Data Management when performing Queries between Services
Microservices are designed to be independent and focus on specific functional requirements. In our e-commerce application example, microservices like product, basket, discount, and ordering need to interact with each other to fulfill customer use cases. These interactions often involve querying data from different services for aggregation or performing certain business logics.
For example, imagine we have "Shopping Cart," "Catalog," and "Pricing" microservices as shown in the image. When a user adds an item to the basket, how should these microservices interact with each other?
Should the Shopping Cart microservice query product data and price information from other microservices synchronously? Or are there alternative approaches to address this challenge?
Furthermore, how can we handle transactional use cases that require interaction with multiple services and the ability to roll back changes if necessary?
To answer these questions and find suitable solutions, we will explore the patterns and principles that address these specific challenges in microservices architecture.
By understanding and applying these patterns, we can effectively manage data interactions and ensure the seamless operation of our microservices-based systems.
Microservice Query Patterns
Here we will learn several microservices query patterns:
Materialized View Pattern
CQRS Design Pattern
Event Sourcing Pattern
Eventual Consistency Principle
1. Materialized View Pattern
When encountering a problem with the "add item into shopping cart" use case in our e-commerce microservices application, we can address it using the "Materialized View Pattern."
The Materialized View Pattern recommends storing a local copy of the required data within the microservice itself. In our case, the shopping cart microservice should have a table that contains a denormalized copy of the data needed from the product and pricing microservices. This local copy of data is often referred to as a Read Model, which is why the pattern is named Materialized View Pattern.
So Instead of the Shopping Basket microservice querying the Product Catalog and Pricing microservices, it maintains its own local copy of that data.
Instead of the shopping cart microservice making queries to the Product Catalog and Pricing microservices each time, it maintains its own local copy of the data.
By adopting this approach, the shopping cart microservice eliminates the need for synchronous cross-service calls. It also enhances the resilience of the microservice because if one of the catalog or pricing microservices is unavailable, it won't block or roll back the entire operation. This pattern reduces the direct dependency on other microservices and improves response time.
How to Use and Where to Use:
To utilize the Materialized View Pattern, you should consider the following steps:
Identify the microservice or use case that frequently requires data from other microservices.
Determine the specific data that needs to be replicated and maintained locally within the microservice.
Create a denormalized copy of the data within the microservice's own storage, such as a dedicated table or data structure.
Implement mechanisms to keep the local copy of the data updated, either through event-driven updates or periodic synchronization with the source microservices.
Modify the microservice's code to retrieve data from the local copy instead of making synchronous calls to other microservices.
Ensure proper error handling and resilience mechanisms are in place to handle scenarios where the source microservices are unavailable or experiencing issues.
The Materialized View Pattern is especially beneficial in situations where frequent cross-service calls result in performance issues or potential failures due to the dependency on other microservices. By maintaining a local copy of the required data, the microservice can operate more efficiently and independently, leading to improved response times and overall system resilience.
2. CQRS Design Pattern
CQRS (Command and Query Responsibility Segregation) is an important pattern to consider when dealing with querying between microservices. It offers a solution to avoid complex queries and inefficient joins.
CQRS is a design pattern that separates the responsibilities of read and update operations for a database. In traditional monolithic applications, a single database is responsible for handling both query operations, involving complex joins, and performing CRUD (Create, Read, Update, Delete) operations. However, as the application grows in complexity, managing these combined responsibilities can become challenging.
By adopting the CQRS pattern, we divide the database operations into two distinct parts: commands and queries. Commands are responsible for handling write operations, such as creating, updating, and deleting data. Queries, on the other hand, focus solely on retrieving data and handling read operations.
The main idea behind CQRS is to optimize the read and write operations separately, as they have different requirements and characteristics. By doing so, we can design specialized read models that are tailored for efficient querying and avoid complex join operations that can impact performance.
How to Use and Where to Use:
To utilize the CQRS pattern effectively, consider the following steps:
Identify the microservice or use case that involves complex querying or requires separation of read and write operations.
Analyze the data access patterns and determine which parts of the system require efficient read operations and which parts focus on write operations.
Design separate models for commands and queries, tailoring them to their specific requirements. This may involve denormalizing data in the read models to optimize query performance.
Implement mechanisms for handling commands and updating the write models accordingly.
Implement mechanisms for handling queries and retrieving data from the read models efficiently.
Establish proper communication and synchronization between the write and read models to ensure consistency.
Consider using event sourcing or event-driven architectures in conjunction with CQRS to capture and propagate domain events.
CQRS is particularly beneficial in scenarios where there is a high volume of read operations or complex querying requirements. By separating the responsibilities of read and write operations, it allows for more optimized and scalable solutions. However, it is important to carefully evaluate the trade-offs, as implementing CQRS adds complexity and introduces eventual consistency between the write and read models.
CQRS can improve the performance and scalability of microservices by providing a clear separation of concerns for read and write operations, enabling efficient querying and avoiding the limitations of complex join queries.
3. Event Sourcing Pattern
When implementing the CQRS pattern, it is often used in conjunction with the Event Sourcing pattern to provide a comprehensive solution.
In the CQRS pattern with Event Sourcing, the focus is on storing events in the write database, which serves as the source-of-truth events database. These events represent changes or actions that have occurred in the system. The read database in the CQRS design pattern then generates materialized views by consuming these events from the write database and converting them into denormalized views.
To understand Event Sourcing, let's start from the beginning. In traditional application architectures, data is stored in databases with the current state of entities. For example, if a user changes their email address in an application, the user's email field is updated with the new address, overriding the existing value. This approach ensures that the latest status of the data is always available.
However, in Event Sourcing, instead of storing just the current state, we focus on capturing and storing the sequence of events that led to the current state. Each event represents a specific action or change in the system. By storing these events, we have a historical record of all the actions that occurred in the system, allowing us to reconstruct the current state by replaying the events.
When combining CQRS with Event Sourcing, the events captured in the write database serve as the primary source of data. The read database, which provides materialized views, consumes these events and transforms them into denormalized views that are optimized for efficient querying.
How to Use and Where to Use:
Here are some considerations for utilizing CQRS with Event Sourcing:
Identify scenarios where capturing the sequence of events and having a historical record is valuable. This is particularly useful in domains with complex business rules, auditing requirements, or the need for temporal analysis.
Implement mechanisms to store events in the write database as the primary source of truth. This may involve using event sourcing frameworks or libraries that provide features for event capture and storage.
Design the read database to generate materialized views from the events stored in the write database. These materialized views should be denormalized and optimized for efficient querying, supporting the specific read operations required by the application.
Establish mechanisms for consuming events from the write database and updating the materialized views in the read database. This may involve event-driven architectures, where services subscribe to events and update the views accordingly.
Ensure proper synchronization and consistency between the write and read databases. Changes in the write database should be reflected in the materialized views to maintain data integrity.
CQRS with Event Sourcing is particularly valuable in domains where historical data and auditing capabilities are essential. It provides a comprehensive approach to capturing and managing the sequence of events, allowing for analysis, reporting, and maintaining a complete history of system actions.
However, it's important to carefully consider the complexity and trade-offs introduced by using Event Sourcing, as it adds additional overhead and may require additional development effort. Evaluate the specific requirements of your domain and choose the appropriate patterns accordingly.
4. Eventual Consistency Principle
The Eventual Consistency Principle is a concept that comes into play when implementing the CQRS pattern in conjunction with the Event Sourcing pattern.
Eventual consistency is often preferred in systems that prioritize high availability over immediate consistency. It implies that the system will eventually become consistent over time, but it does not guarantee instant consistency. This latency in achieving consistency is referred to as the Eventual Consistency Principle, which suggests that the system aims to be consistent after a certain period.
When considering the consistency levels of microservices databases, we can refer to the CAP theorem. The CAP theorem states that we need to determine the desired "consistency level" for our databases. There are two types of consistency levels:
Strict Consistency: This level aims to ensure that when data is saved, it immediately affects and is visible to every client. An example of strict consistency is a debit or withdrawal transaction in a bank account.
Eventual Consistency: This level acknowledges that when data is written, it may take some time for clients to read the updated data. An example of eventual consistency is the view counters on YouTube videos, where different sessions may display different view counts.
With the implementation of the CQRS Design Pattern and Event Sourcing, when a user performs an action in the application, the action is saved as an event in the event store. This data is then converted into a reading database by following the publish/subscribe pattern using message brokers. Finally, the data is denormalized into a materialized view database, which allows efficient querying from the application. During the process of converting the data into denormalized form, the system exhibits eventual consistency. This process is referred to as the Eventual Consistency Principle.
How to Use and Where to Use:
Consider the following points for utilizing eventual consistency in the context of CQRS with Event Sourcing:
Evaluate the requirements and trade-offs of your system. Eventual consistency is suitable for scenarios where high availability and scalability are more important than immediate consistency.
Identify areas in your application where eventual consistency can be applied. This may include read-heavy operations, reporting, or scenarios where immediate consistency is not critical.
Implement appropriate mechanisms to handle the eventual consistency, such as using message brokers for asynchronous communication and updating materialized views periodically.
Educate users and stakeholders about the eventual consistency nature of the system, ensuring they understand the potential time lag between data updates and read availability.
Monitor and measure the impact of eventual consistency on user experience and system behavior. Fine-tune the system based on feedback and performance metrics to strike the right balance.
Eventual consistency can be a powerful concept in distributed systems, allowing for improved scalability and performance. However, it's essential to consider the specific requirements of your application and the expectations of your users when deciding where and how to apply eventual consistency.
In this article, we have learned different Microservices Query Patterns, how to use and where to use them.