Odsh
Odsh

Reputation: 697

Design choice for a microservice event-driven architecture

Let's suppose we have the following:

DDD aggregates A and B, A can reference B.

A microservice managing A that exposes the following commands:

A microservice managing B that exposes the following commands:

A successful creation, deletion, link or unlink always results in the emission of a corresponding event by the microservice that performed the action.

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

  1. A and B will always eventually be consistent with each other. By consistency, I mean A should not reference B if B doesn't exist.
  2. The events from both microservices can easily be projected in a separate read model on which queries spanning both A and B can be made

Specifically, the following examples could lead to transient inconsistent states, but consistency must in all cases eventually be restored:

Example 1

Example 2

Example 3

I have two solutions in mind.

Solution 1

Solution 2:

EDIT: Solution 3, proposed by Guillaume:

The advantage I see for solution 2 is that the microservices don't need to keep track of of past events emitted by the other service. In solution 1, basically each microservice has to maintain a read model of the other one.

A potential disadvantage for solution 2 could maybe be the added complexity of projecting these events in the read model, especially if more microservices and aggregates following the same pattern are added to the system.

Are there other (dis)advantages to one or the other solution, or even an anti-pattern I'm not aware of that should be avoided at all costs? Is there a better solution than the two I propose?

Any advice would be appreciated.

Upvotes: 5

Views: 1309

Answers (3)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57387

Microservice A only allows linking A to B if it has previously received a "B created" event and no "B deleted" event.

There's a potential problem here; consider a race between two messages, link A to B and B Created. If the B Created message happens to arrive first, then everything links up as expected. If B Created happens to arrive second, then the link doesn't happen. In short, you have a business behavior that depends on your message plumbing.

Udi Dahan, 2010

A microsecond difference in timing shouldn’t make a difference to core business behaviors.

A potential disadvantage for solution 2 could maybe be the added complexity of projecting these events in the read model, especially if more microservices and aggregates following the same pattern are added to the system.

I don't like that complexity at all; it sounds like a lot of work for not very much business value.

Exception Reports might be a viable alternative. Greg Young talked about this in 2016. In short; having a monitor that detects inconsistent states, and the remediation of those states, may be enough.

Adding automated remediation comes later. Rinat Abdullin described this progression really well.

The automated version ends up looking something like solution 2; but with separation of the responsibilities -- the remediation logic lives outside of microservice A and B.

Upvotes: 3

guillaume31
guillaume31

Reputation: 14080

I will start with the same premise as @ConstantinGalbenu but follow with a different proposition ;)

Eventual consistency means that the whole system will eventually converge to a consistent state.

If you add to that "no matter the order in which messages are received", you've got a very strong statement by which your system will naturally tend to an ultimate coherent state without the help of an external process manager/saga.

If you make a maximum number of operations commutative from the receiver's perspective, e.g. it doesn't matter if link A to B arrives before or after create A (they both lead to the same resulting state), you're pretty much there. That's basically the first bullet point of Solution 2 generalized to a maximum of events, but not the second bullet point.

  • Microservice B listens for "A linked to B" events and, upon receiving such an event, verifies that B exists. If it doesn't, it emits a "link to B refused" event.

You don't need to do this in a nominal case. You'd do it in the case where you know that A didn't receive a B deleted message. But then it shouldn't be part of your normal business process, that's delivery failure management at the messaging platform level. I wouldn't put this kind of systematic double-check of everything by the microservice where the original data came from, because things get way too complex. It looks as if you're trying to put some immediate consistency back into an eventually consistent setup.

That solution might not always be feasible, but at least from the point of view of a passive read model that doesn't emit events in response to other events, I can't think of a case where you couldn't manage to handle all events in a commutative way.

Upvotes: 2

Constantin Galbenu
Constantin Galbenu

Reputation: 17703

Your solutions seem OK but there are some things that need to be clarified:

In DDD, aggregates are consistencies boundaries. An Aggregate 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 whole system is in a permitted permanent state from the business point of view. There are moments when the system as whole is in a not-permitted state. This is OK as long as eventually it will transition in a permitted state. Here comes the Saga/Process managers. This is exactly their role: to bring the system in a valid state. They could be deployed as separate microservices.

One other type of component/pattern that I used in my CQRS projects are Eventually-consistent command validators. They validate a command (and reject it if it is not valid) before it reaches the Aggregate using a private read-model. These components minimize the situations when the system enters an invalid state and they complement the Sagas. They should be deployed inside the microservice that contains the Aggregate, as a layer on top of the domain layer (aggregate).

Now, back to Earth. Your solutions are a combination of Aggregates, Sagas and Eventually-consistent command validations.

Solution 1

  • Microservice A only allows linking A to B if it has previously received a "B created" event and no "B deleted" event.
  • Microservice A listens to "B deleted" events and, upon receiving such an event, unlinks A from B.

In this architecture, Microservice A contains Aggregate A and a Command validator and Microservice B contains Aggregate B and a Saga. Here is important to understand that the validator would not prevent the system's invalid state but only would reduce the probability.

Solution 2:

  • Microservice A always allows linking A to B.
  • Microservice B listens for "A linked to B" events and, upon receiving such an event, verifies that B exists. If it doesn't, it emits a "link to B refused" event.
  • Microservice A listens for "B deleted" and "link to B refused" events and, upon receiving such an event, unlinks A from B.

In this architecture, Microservice A contains Aggregate A and a Saga and Microservice B contains Aggregate B and also a Saga. This solution could be simplified if the Saga on B would verify the existence of B and send Unlink B from A command to A instead of yielding an event.

In any case, in order to apply the SRP, you could extract the Sagas to their own microservices. In this case you would have a microservice per Aggregate and per Saga.

Upvotes: 2

Related Questions