Data Consistency Across Microservices

Viewed 13074

While each microservice generally will have its own data - certain entities are required to be consistent across multiple services.

For such data consistency requirement in a highly distributed landscape such as microservices architecture, what are the choices for design? Of course, I do not want shared database architecture, where a single DB manages the state across all the services. That violates isolation and shared-nothing principles.

I do understand that, a microservice can publish an event when an entity is created, updated or deleted. All other microservices which are interested in this event can accordingly update the linked entities in their respective databases.

This is workable, however it leads to a lot of careful and coordinated programming effort across the services.

Can Akka or any other framework solve this use case? How?

EDIT1:
Adding the below diagram for clarity.
Basically, I am trying to understand, if there are available frameworks today that can solve this data consistency problem.

For the queue I can use any AMQP software such as RabbitMQ or Qpid etc. For the data consistency framework, I am not sure if presently Akka or any other software can help. Or is this scenario so uncommon, and such an anti-pattern that no framework should be ever needed?
enter image description here

7 Answers

Same problem here. We have data in different microservices and there are cases where one service needs to know if there is a specific entity in another microservice. We do not want the services to call each others to complete a request because this adds response time and multipies downtimes. Also it adds a nightmare of coupling depth. The client should not decide about business logic and data validation/consistency either. We also do not want central services like "Saga Controllers" to provide for consistency between services.

So we use a Kafka message bus to inform observing services of state changes in "upstream" services. We try very hard not to miss or ignore any messages even in error conditions and we use Martin Fowler's "tolerant reader" pattern to couple as loosely as possible. Still sometimes services are changed and after the change they might need information from other services that they might have emitted on the bus before but they are now gone (even Kafka cannot store forever).

We decided for now that each Service be split in a pure and decoupled web service (RESTful) that does the actual work and a separate Connector-Service which listens to the Bus and may also call other services. This Connector runs in the background. It is only triggered by bus messages. It then will try to add data to the main service via REST calls. If the service responds with a consistency error, the connector will try to repair this by fetching the needed data from the upstream service and inject it as needed. (We cannot afford batch-jobs to "synchronize" data en block, so we just fetch what we need). If there are better ideas, we are always open, but "pull" or "just change data model" is not what we consider feasible...

Managing Data Access Between Modules

What Is A Module?

A module is a piece of software that has a functionality by itself. A module can be deployed together with others modules as a monolith or seperately as a microservice. When defining a module one should be careful because managing data access between modules becomes harder. Thus it requires a good amount of experience in a specific field to decide. It's better to make the mistake of merging "actual two modules" into one rather than seperating a "single module" into two. Because if you seperate a module into two when you shouldn't there are going to be lots of data acces between these modules, which can be pretty hard to manage especially if there is transactional logic. But sometimes it is necessary to make modules espeacilly when things start to get big. Here is a decision tree I use to decide which kind of strategy I must choose:

Decision Tree For Data Reads

If there are two services such that A depends on B...

  • and they are in the same module...
    • and A requires a simple data read: A should use the interface of B which is implemented by direct database read.
    • and A requires complex data read1 : direct database table join should be used for reads.
  • and they are in different modules...
    • and A requires simple data read...
      • and they are deployed as monolith: A should use the interface of B which is implemented with direct database read.
      • and they are deployed as microservices: A should use the interface of B which is implemented with a http client.
    • and A requires a complex data read...
      • and they are deployed as monolith: A should copy data from B in a different format optimized for its use case by consuming from an in-memory event bus.
      • and they are deployed as microservices: A should copy data from B in a different format optimized for its use case using an event bus consumer.

Decision Tree For Data Writes

If there are two services such that A depends on B...

  • and they are deployed as a monolith: interface of B should be implemented with direct database write.
  • and they are deployed as microservices... (might require distributed transaction management between services)
    • and A requires simple data write: A should use the interface of B which is implemented with a HttpClient.
    • and A requires complex data write2 : A should use the interface of B which is implemented with an event bus poducer.

complex data read1: batch processing, ordering/filtering aftrer join, transaction management etc. complex data write2: IO intensive, CPU intensive, Network intensive

Related