Design Choice for Event-Driven Microservice Architecture

Suppose we have the following:

Aggregates DDD A and B, A may refer to B.

Microservice A management, which provides the following commands:

  • create a
  • remove A
  • link A to B
  • disconnect A from B

Microservice B Management, which provides the following commands:

  • create B
  • remove B

Successfully creating, deleting, linking or disconnecting always leads to the output of the corresponding event using the microservice that performed the action.

What is the best way to create an event-driven architecture for these two microservices so that:

  • A and B will always end up matching each other. By consistency, I mean that A should not refer to B if B does not exist.
  • Events from both microservices can be easily projected in a separate reading model, on which queries can be executed covering both A and B

In particular, the following examples can lead to short-term inconsistent states, but in all cases the sequence must ultimately be restored:

Example 1

  • Initial Consent State: A exists, B does not support, A is not associated with B
  • Command: Link A to B

Example 2

  • Initial agreed state: exists A, exists B, A is associated with B
  • Command: delete B

Example 3

  • Initial agreed state: exists A, exists B, A is not associated with B
  • Two simultaneous commands: link A to B and delete B

I have two solutions.

Solution 1

  • Microservice A only allows you to associate A with B if it previously received the event "B created" and did not delete "B".
  • Microservice B only allows you to delete B if it has not previously received the event “A associated with B”, or if this event was followed by the event “A unrelated to B”.
  • Microservice A listens for events "B deleted" and, after receiving such an event, disconnects A from B (for a race condition in which B is deleted before it receives A associated with event B).

Solution 2:

  • Microservice A always allows you to associate A with B.
  • Microservice B listens for events “A connected to B” and, after receiving such an event, checks if B exists. If this is not the case, it throws a “link to B failed” event.
  • Microservice A listens for events "B deleted" and "link to B excluded" and, upon receipt of such an event, separates A from B.

EDIT: Solution 3 proposed by Guillaume:

  • Microservice A only allows you to bind A to B if the B deleted event was not previously received.
  • Microservice B always allows you to delete B.
  • Microservice A listens for events "B deleted" and, after receiving such an event, disconnects A from B.

The advantage that I see for solution 2 is that microservices do not need to keep track of past events emitted by another service. In solution 1, basically every microservice should support a readable model of the other.

A potential disadvantage of solution 2 may be the added complexity of designing these events in the reading model, especially if additional microservices and aggregates following the same template are added to the system.

Are there other (dis) advantages for one or the other solution, or even an anti-pattern that I don't know about, should be avoided at all costs? Is there a better solution than two suggestions?

Any advice would be appreciated.

+4
source share
3 answers

Microservice A only allows you to associate A with B if it previously received the event "B created" and not "B deleted".

There is a potential problem here; consider the race between the two posts link A to B and B Created . If the message B Created occurs first, then everything will be contacted as expected. If B Created is second, then the link will not happen. In short, you have business conduct that depends on your plumbing.

Udi Dahan, 2010

A microsecond time difference should not affect underlying business behavior.

A potential disadvantage of solution 2 may be the added complexity of designing these events in the reading model, especially if additional microservices and aggregates following the same template are added to the system.

I don't like this complexity at all; it sounds like a lot of work for a not-so-big business value.

Exclusion reports can be a viable alternative. Greg Young talked about this in 2016 . Briefly; having a monitor that detects inconsistent states, and restoring these states may be enough.

Adding automated remediation occurs later. Rinat Abdullin described this progression in detail.

The automatic version ends up looking like solution 2; but with a separation of duties - the recovery logic lives outside microservice A and B.

+2
source

Your solutions look good, but there are some things to clarify:

In DDD, aggregates represent consistency boundaries. The unit is always in a consistent state, no matter what command it receives, and if that command succeeds or not. But this does not mean that the entire system is in a permissible permanent state from a business point of view. There are times when the system as a whole is in an unacceptable state. This is normal until it finally goes into an allowed state. Here comes the Saga / Process Managers . This is their role: to bring the system into the correct state. They can be deployed as separate microservices.

Another type of component / template that I used in my CQRS projects is Ultimately, consistent team checks . They validate the command (and reject it if it is not valid) before it reaches the Unit using a private reading model. These components minimize situations when the system enters an unacceptable state, and they complement the sagas. They should be deployed inside the microservice that contains Aggregate as a layer on top of the domain (aggregate) level.

Now back to Earth. Your decisions are a combination of Aggregates, Sagas and finally agreed teams.

Solution 1

  • Microservice A only allows you to associate A with B if it previously received the event "B created" and did not delete "B".
  • Microservice A listens for events "B deleted" and, after receiving such an event, disconnects A from B.

In this architecture, Microservice A contains Aggregate A and a Command validator , while Microservice B contains Aggregate B and a Saga . It is important to understand here that the validator will not interfere with the system unacceptable state, but will only reduce the likelihood.

Solution 2:

  • Microservice A always allows you to associate A with B.
  • Microservice B listens for events “A connected to B” and, after receiving such an event, checks if B exists. If it is not, it emits a “link to failure B” event.
  • Microservice A listens for events "B deleted" and "link to B excluded" and, upon receipt of such an event, separates A from B.

In this architecture, Microservice A contains Aggregate A , while Saga and Microservice B contains Aggregate B , as well as a saga. This solution can be simplified if Saga on B verifies the existence of B and sends the Unlink B from A command to instead of giving an event.

In any case, to apply SRP , you can extract the Sagas into your own microservices. In this case, you will have a microservice for each unit and each saga.

+2
source

I will start with the same assumption as @ConstantinGalbenu, but follow another suggestion;)

Ultimately, sequence means that the entire system ultimately converges to a consistent state.

If you add to this “regardless of the order in which messages are received,” you have a very strong statement, according to which your system will naturally strive for the ultimate consistent state without the help of an external process manager / saga.

If you do the maximum number of operations commutative from the point of view of the receiver, for example. it doesn’t matter if link A to B comes before or after create A (they both produce the same result), you are pretty much there. This is basically the first bullet point of solution 2, generalized to the maximum of events, but not the second bullet point.

  • Microservice B listens for events "A associated with B" and, upon receiving such an event, checks for the existence of B. If it is not, it emits a link to B refused. "

You do not need to do this in the nominal case. You would do this if you knew that A did not receive the message B deleted . But then it should not be part of your normal business process, it is delivery failure management at the messaging platform level. I would not put such a systematic double check of everything on the microservice where the source data came from, because everything becomes too complicated. It looks like you are trying to return some immediate sequence to a sequential setup.

This solution is not always possible, but at least from the point of view of a passive reading model that does not generate events in response to other events, I cannot think of a case where you could not manage to handle all events in a commutative way.

+2
source

Source: https://habr.com/ru/post/1273701/


All Articles