Microservices are typically composed of a database and a model for Create, Read, Update, and Delete operations, commonly known as CRUD (Create, Read, Update, and Delete). In certain contexts, there may be an additional model for specific read operations, as shown on the left side of Figure 1.
There are three common problems that occur when implementing read operations in a microservices architecture:
The CQRS (Command and Query Responsibility Segregation) pattern addresses these problems by separating create, update, and delete operations (command operations) from read operations (queries) within a domain, using different models and databases, as seen on the right side of Figure 1. According to Bertrand Meyer, author of "Object-Oriented Software Construction", "asking a question should not change the answer," to demonstrate that separating command operations from read operations is valid, as read operations do not alter data state. Similarly, applying this approach can result in separating operations at the microservice level, i.e., implementing a microservice for reads (query service), which will be discussed further.
Applying the CQRS pattern involves synchronizing the database for reads with the one for command operations. Thus, for each create, update, and delete operation, the command model publishes an event, which is then subscribed to by the read model to synchronize the read database with each change.
It is important to highlight that, despite the separation of create, update, and delete operations from read operations, the command model may still contain some simple read operations based on primary keys, which do not involve joining large amounts of data.
Regarding the first problem mentioned earlier, it refers to the context of an inappropriate implementation of the API composition pattern. It occurs when the compositing microservice needs to join and filter numerous data in memory because the invoked microservices do not do so in their databases, making the main read operation very slow and inefficient.
An example of this is read operations given a set of attributes as criteria that are not present in all microservices involved in API composition.
For instance, suppose the functionality to "retrieve a customer's order list" given a set of keywords through an API Composition involving multiple microservices, as seen in Figure 2.
Since the "delivery" and "payments" microservices cannot perform the read operation based on keywords in their databases, all records associated with the customer are returned to the compositing microservice.
Due to this, in the compositing microservice (green-tinted figure), the functionality now includes the complexity of filtering numerous data in memory from the "delivery" and "payments" microservices, along with data already filtered by the "orders" and "kitchen" microservices, followed by the normal functionality of joining results. Therefore, this added complexity in such contexts makes the API Composition pattern inefficient.
The resolution to the previously mentioned problem involves applying the CQRS pattern as an alternative to API composition. In fact, applying this pattern in this context involves implementing a read microservice (query service).
A read microservice, as seen in Figure 3, is responsible for implementing read operations over a data view sourced from one or more microservices. This microservice persists the necessary data for read operations in an appropriate structure and technology, synchronized with the owning microservices by subscribing to events emitted with each change.
Applying the CQRS pattern to resolve the second problem is very similar to the previous solution, as it also allows for adapting the best data structures and database technologies to ensure more efficient read operations.
Adapting structure and technology may refer to read operations involving geographical locations and indexes, where certain database technologies do not support them. Thus, it is not justified to modify a microservice's database for such operations but to apply the CQRS pattern with the previously mentioned approach, i.e., creating a read microservice (query service). Thus, the read microservice implements a database structure and technology that supports geographical locations and allows more efficient read operations.
Finally, the last problem relates to the separation of concerns, as deciding which microservice should implement a read operation is not solely based on the ownership of the involved data. For certain read operations, even if a microservice contains most of the required data, it does not imply it is the most suitable for the responsibility of that operation, avoiding responsibility overload.
For example, in a meal delivery system, where the operation to obtain the closest restaurants to a particular user needs to be implemented, the system contains a microservice called "restaurant," responsible for maintaining the data necessary for this operation, i.e., the restaurant locations. However, the main responsibility of this microservice, as mentioned, is to allow restaurant owners to maintain their data, such as: name, location, cuisines, menus, and hours. The operation to find the nearest restaurants does not fit this microservice's responsibilities, as it involves executing a critical operation with large volumes of data.
Applying the pattern in this context to resolve the problem involves implementing a read microservice (query service), similar to previous solutions, as it delegates the responsibility of executing this operation to a new or more suitable existing microservice.
With the presented problems and solutions, it is clear that the challenges of executing read operations in microservices architectures are not always about spanning multiple microservices. In fact, as seen with the second and third problems, they refer to a single microservice.
One advantage of applying this pattern is the efficiency of implementing read operations that return data from multiple microservices. This pattern avoids problems with joining numerous data in memory that occur with the alternative pattern, resulting in more efficient read operations.
Another advantage is the ability to adapt the best data structures, technologies, and resources according to the operation's requirements. For instance, in terms of resources, there are typically more read requests than write requests, so by separating the responsibilities, scaling can be done independently.
The separation of concerns is an advantage, as previously mentioned, because it divides the models for command and read operations, as well as their respective databases. This allows for more focused and efficient development of models.
On the other hand, implementing this pattern comes with some disadvantages. The first disadvantage is increased complexity, as more components need to be operationalized and maintained, such as operation models, databases, new microservices, among others.
Another disadvantage is that communication between command and read models depends on the network, which can lead to failures, latencies, and other issues that may result in data being out of date. However, a possible solution to mitigate this problem is to return the data added or changed in the command operation.
According to Martin Fowler, there are instances of applying the CQRS pattern in contexts with very competent programming teams where there was a drag on productivity and an increased risk to the project. Thus, it is important to emphasize that, whenever possible, i.e., when the previously described problems do not exist, the API composition pattern should be applied. On the other hand, the CQRS pattern should only be applied when these problems occur, as misapplying this pattern leads to increased complexity, risk, and reduced productivity.
Finally, the open-source framework Axon can be used for implementing the CQRS pattern. Additionally, as previously mentioned, this pattern requires message or event sending, so transactional messaging patterns and event sourcing can be implemented for this purpose, using frameworks such as Eventuate Tram and Eventuate Local.
Dissertation: "Study on the importance of the Principles and Patterns of Microservices Architectures", 2023
Website: "Microservice Architecture"
Website: "Martin Fowler"