Architecture concerns using minimal aggregates in a long running transaction

143 Views Asked by At

I am trying to follow Vaughn Vernon guidelines on aggregate design.

I understand that keeping aggregates as small as possible is advised for the many reasons mentioned in his series and by many DDD practitioners.

But this creates an issue handling unrelated command properties inside an aggregate host.

I have an incoming command that contains the necessary information for the system to conduct a transaction in full. The information contained in the command does not fit into a single aggregate since they are minimal.

So, I am considering this to be a long running transaction. I am controlling the entire transaction flow using the saga pattern.

Every host that wraps an aggregate involved in this long running transaction raises a domain event. That domain event is then intercepted by the saga which in turn issues the subsequent command and so on and so forth until the business transaction either gets carried successfully or fails altogether.

If we imagine a scenario where we have a command of the kind:

Send Message X from Participant Y in Conversation Z

This command for instance would be intercepted by the host of the Message aggregate.

This host would rehydrate the Message aggregate, tells it to validate the first part of the transaction:

Send Message X from participant Y.

The same host that intercepted the command would then have to raise a domain event say

MessageCreated, the issue here is that MessageCreated event would need to carry the rest of the business transaction details

In conversation Z

For the subsequent aggregate to execute the rest of the transaction.

Since Message aggregate does not care about ConversationId its host would need to keep that information (needlessly from the perspective of the aggregate).

When the aggregate approves the transaction, the host would then have to create a domain event say MessageCreated * and attach ConversationId to it.

My concern here is that by keeping information that is not necessary to the execution of the business transaction inside the host would over time accumulate with more business requirements coming along at best and would probably create coupling in logic between hosts at worst.

To my question:

Is this considered a safe approach to handle long running transaction and if not what would be a better alternative?

2

There are 2 best solutions below

0
On

That irrelevant data needs to be propagated between operations for the purpose of future operations suggests some redesign may be beneficial. This is the first I've heard of the aggregate design but it seems like the general idea is operations on an aggregate should not leave any part of it in an invalid state in the context of the aggregate. The general idea behind saga is a change in state is an event, events should be broadcast when they happen, and operations should be triggered by certain events.

One way to approach this is to reconsider the decision that a message does not depend on the conversation. Ex:

Message (uuid, content, timestamp, participant id, conversation id)

  1. Rules:
  2. Create Events: MESSAGE_CREATED
  3. Listen Events:

Participant (uuid, first name, last name, email)

  1. Rules:
  2. Create Events: PARTICIPANT_CREATED, EMAIL_CHANGED
  3. Listen Events:

Conversation (uuid, list of message ids, list of participant ids)

  1. Rules: All messages must belong to a participant in the conversation.
  2. Create Events: CONV_CREATED, PERSON_ADD, PERSON_RM, MESSAGE_ADDED
  3. Listen Events: MESSAGE_CREATED

Given these aggregates and events, one approach is:

  1. Receive Command Send Message X from Participant Y in Conversation Z
  2. Create Message(uuid, X, Y, Z) -> MESSAGE_CREATED
  3. Conversation receives event -> MESSAGE_ADDED (if participant in conversation)
0
On

It is not clear from your question how and why you designed your aggregates this way. You mention that they have to be minimal, but I wouldn't think this is the most important aspect. Aggregates need to fulfil their goal. The size matters only when they are or can become too big to be usable. For example, a Conversation that could have several years worth of messages inside is probably not a good design. But what might tell you that is not a good design is not only its ever-growing nature but the fact that it most likely doesn't need all the messages to fulfil its goal (there's probably no business logic which involves all messages in the conversation). Maybe you only need the last few messages or the messages that have not been received yet by the other participants. After that, you can probably remove the messages from the Conversation and leave them in a read-only model for historic/display purposes.

What I'm trying to say is that one solution might be to redesign your aggregates so that you don't have this problem in the first place.

On the other hand, you mention that you use a Saga pattern, but it's not clear how you do it based on the explanation, as it seems that the aggregates are coordinated by events, instead of by a saga.

If you need to coordinate aggregates with a saga, the Command will be handled by the saga. Then, you'll face one of two scenarios:

  1. the operations to be coordinated don't depend on each other
  2. the operations depend on each other and they need to be executed in a certain order (or rely on data from the previous operations)

In the first scenario, the saga will just handle the command and send "smaller" commands to all the aggregates that need to be coordinated. These aggregates will execute the operation and publish an event or reply to the command sender, which would be a design choice. The saga then collects all the responses and when it has received all the responses the operation has completed (obviously, you'll need to handle scenarios where some of the operations fail or never complete).

In the second scenario, the saga will handle the command and store all the necessary information internally. Then send a command to the first aggregate and wait for the reply or event. When received, it will use the information from the event plus the information received in the command to send the following command in the process, and so on.

In both scenarios, the saga should be able to detect when one or more of the steps don't succeed and send other commands to compensate or revert the steps that did succeed.