preface

Query is the basic function of software system. In the case of single application and using a database, query operation can often be realized by writing SQL and using necessary indexes. In microservices, however, operations are more complex because of the autonomy and isolation of microservices, and queries often require retrieving data from private databases scattered across services.

There are two common ways to implement queries in microservices:

  • API composition: Clients invoke individual services and combine the returned query results. This method is straightforward and simple
  • CQRS mode: Command query responsibility separation mode, which maintains one or more views of the data. This approach is more powerful, but also more complex to implement and maintain

Let’s look at the two patterns in turn

API composite

The API composite pattern is

The caller invokes the service with the data and combines the returned query results

The API composition pattern is most intuitive when a query requires data not just from its own service but from multiple services

It consists of two types of roles:

  • API combinator: It gets the data by calling the data provider and assembles it
  • Data provider: a service that owns part of the data

The API combinator can be either a data presentation layer, such as on the browser or mobile. It can also be a back-end service, such as an API Gateway, or a microservice

Points to consider

This may seem simple, and a working query would probably do the same, but there are two issues to consider

  1. Determine which component acts as the API assembler
  2. How to write efficient combinatorial logic

For question one, there are generally three choices

  • Data presentation layer: This is generally not a good option. First, it is a burden on the front end, which needs to be more focused on user interaction and data population rather than assembling and aggregating data from different services, which can be left to the back end team. Second, it is less efficient because it makes multiple requests from the external network to the service internal machine room, which is certainly not as efficient as calls from inside the machine room. Inefficiency inevitably affects the user experience

  • API Gateway layer: the front end sends the request to the gateway service, which then calls multiple back-end services to get the data, and assembles the data back. Because gateway services and back-end services are usually in the same room, the invocation overhead is low. This can significantly alleviate the inefficiencies of the previous scheme. But it can complicate the gateway logic

  • Independent data aggregation service: It is also possible to use a separate data aggregation service behind the API gateway, which reduces the complexity of the API gateway, but also increases the number of internal hops

For the second problem, the distributed system needs to reduce the delay between services. API combinator should call data services in parallel as much as possible, and adopt some tools to ensure the correctness of the parallel call, such as Java’s CompletableFuture, go channel, etc. If there are dependencies between calls, the required parts are called sequentially in the order of dependency

An API gateway or a separate data aggregation service is recommended

The advantages and disadvantages

The API combinator has the advantage of being simple and intuitive, but it also has some disadvantages:

  • Certain invocation costs: The API compositor for microservices needs to query multiple data services, resulting in additional network request overhead compared to a single architecture
  • Reduced availability: Both the API combinator and the data provider need to be available for the entire query to be available. Its availability is clearly less than that of a single service. For example, if there are 4 servers and each server is 99% available, then the overall availability is (99%) ^4 = 96%! There are certainly ways to improve availability, such as the API combinator returning default data or caching data when the data service is unavailable
  • Data consistency problem: In a single application, when a query requires data in the same database, the MVCC mechanism enables the data of a single query to be obtained from a consistent view by relying on the isolation of the database. This is mandatory for some scenarios, such as queries for database backups. But getting a consistent view across microservices is not as easy and requires additional processing

Let’s look at another query solution: CQRS

CQRS

Command Query Responsibility Segregation indicates that

Queries are implemented using events to maintain read-only view data replicated from multiple services

It divides the module that uses data into two parts:

  • Command end: responsible for data creation, modification, deletion operations
  • Query side: responsible for data query operation

The query end keeps the data synchronized with the command end by subscribing to the data update events published by the command end

The advantages and disadvantages

In non-CQRS applications, queries and commands read and write from the same library, but in fact queries and commands are two different application scenarios. Separating them has the following benefits:

  1. Make full use of your strengths: The command side needs to ensure transactional data and write performance, while the query side needs to support various query requirements, support efficient query, and demand higher read performance. For example, the command end uses a relational database to support transactions, and the query end uses ES and other components to support various forms of efficient query
  2. Divide teams properly: Separating the query and command ends allows different teams to be responsible for different businesses. For example, the command side is assigned to the business team, and the query side is assigned to the dedicated search team, achieving business isolation and cohesion, to improve production efficiency
  3. Performance improvement: In addition, this method of querying is more efficient than the CQRS API combination model, because the program does not need to call the interface to retrieve data from other services on each query, but instead queries the local database. The local database can also support various query patterns, and the API combinator pattern may also require coordination with other teams to develop new interfaces to support new queries

CQRS also has some disadvantages:

  1. Higher complexity: Because the query side needs to service one or more additional databases of different types, it increases the complexity of operation and maintenance and the cost of learning for developers
  2. Delay of data replication: Data is asynchronously replicated from the command end to the query end by means of events. The replication speed is very fast, but the delay may be high due to the impact of load and network environment, but the view is consistent in the end. This mode does not guarantee write-to-read effect. If you need to read the data immediately after writing, you need to do some additional processing, such as reading the command end in a short time, or caching a newly saved data on the client. Therefore, CQRS mode is suitable for use in scenarios that do not require so strict real-time performance. Fortunately, most scenes don’t need to be strictly real-time

Points to consider

When designing the CQRS mode. There are some points to consider:

  1. Query database selection: Generally, NoSQL database is a better choice, because the query end of CQRS mainly uses the rich query methods provided by it. For example, redis can be used for primary key query, mongodb can be used for document query, and Neo4j can be used for graph query. But the use of SQL database is also considered in the scope, because the development, operation and maintenance personnel are usually more familiar with THE SQL database, the start cost is low, and the SQL database can meet the report requirements in a variety of complex queries
  2. Support for update operations: The query side needs to consume the update, insert, and delete events of the original data to apply to the view data, so the query side database needs to support update operations. Some database update methods are limited. For example, Redis only supports updating by primary key. Therefore, it is better to update by primary key when designing update events
  3. Concurrent processing: When updates to the same record arrive at the same time, concurrency security needs to be considered. If you use the read, update, and write mode, there must be concurrent security problems, resulting in update loss. You need to lock the entire operation to make it serial. Or use an optimistic lock. If the optimistic lock verification fails, retry
  4. Idempotent: The query side uses data update events to update data, and common MQ typically supports at least one message guarantee, so it is necessary to implement idempotent message processing on the message consuming side, the query side. Some operations are inherently idempotent, such as deleting a record by ID. But some operations, if not idempotent, can cause business problems, such as increasing a bank account balance. Processing of nonidempotent events requires a unique ID to detect and discard repeated events.

In SQL database supports transactions, as the only id alone, is stored in the table is combined with the only index can be the only id record insertion and which data update operation in a transaction execution, if id failed test, the update operation is not executed, if the same update operation fails, id will be inserted into the failure, next time you can also use the id

However, most NoSQL databases do not support transactions, so consider using a single-threaded database such as Redis. Use lua scripts to encapsulate ID detection and data update operations to make them atomic

conclusion

This paper summarizes two ways to implement query in microservice distributed architecture. API composition pattern is common, and its implementation is intuitive and simple. If there is no special requirement, API composition pattern is preferred. The CQRS pattern provides higher performance, supports richer query patterns, and isolates technical teams. At the same time, the data replication delay is not controllable and the implementation is complex

Reference documentation

1. Microservice structure design mode