I want to understand the consequences of allowing handlers to be direct or indirect source of new domain events.
In all examples I see in the literature Domain Events are usually published by Aggregate Roots. Actually, events may be just returned and then later be published before or/and after transaction is committed.
Let's consider the flow:
AR returns event
-> event is published
-> handler modifies another AR which belongs to same Bounded Context
-> modified AR produces another event
-> event is published/persisted
-> ...
-> commit (all or nothing)
.
Event dispatching and processing continues until no new events occur. (It somewhat reminds me database triggers behaviour; and triggers are hated for cascades of state changes, but to be honest, I find eventual consistency with all these event and message flows to be not that easier to reason about.)
Ok, you may say:
"Changing multiple AR's in same transaction is an indication of broken AR design as its main task is to be a transaction boundary."
I may agree and suggest another scenario:
Imagine two requirements:
- Client address must be unique.
- (recently introduced req.) Some addresses (not only client's) must be automatically substituted with another address.
We have multiple ARs and thus multiple workflows where address is set. Address is modelled as a value object, it is validated and created by Address IAddressService.New(string address)
.
The logic of substitution partially depends on AR's state and some external state.
Option1.
Create a set of address services each working with particular AR type and returning coerced address. Unfortunately, some ARs require an Address to be provided as a process of their creation. It means service has to work with 'raw' data. This would be a possible solution, unless we needed to create an event about address substitution and reference AR in this event. It is impossible, if AR's Id
is generated by the store.
Option 2.
Adopt eventual consistency. Save AR as is, run address substitution after commit, maybe fail if unique constraint is broken.
Personally, I have no idea how to notify downstream Bounded Context about the failure when this BC belongs to third party - they just make a call to HTTP endpoint and expect either success or failure.
Option 3.
Let handler change address in AR. The whole flow is:
AR is created
-> event is handled by the handler which checks for uniqueness of address
, then same event is handled by new handler, and it changes AR's address
+emits new event AddressSubstituted about performed substitution
-> AR emits a regular AddressChanged event
-> check for uniqueness is done once again
, but no need for substitution this time
-> no new events are emitted
-> commit
-> Id is assigned to AR
-> publish/persist AddressSubstituted event
(this event has a reference to AR, thus when publishing/persisting, Id is already has a valid value).
Note that all critical parts run inside same transaction. Handler produces one event itself AddressSubstituted
, and leads to another event indirectly - AddressChanged
.
AddressSubstituted
is published outside transaction, however it is still possible to save everything within same transaction. EF core is quite a clever beast. In case of configured relationships it will figure out the order of persistence, and will first save AR, then use its Id
for saving. EF can also generate Guid
automatically when AR is added to DbContext (but still after AR is created).
It's worth mentioning that order of execution of event handlers may or may not be important, but it is a separate subject for discussion.
Pros of this solution: - Old code is almost not touched, just develop new handlers. - Atomicity. Caller is immediately notified about the status of operation. - Intent of the change is captured.
Cons:
- Extremely hard to reason about the flow (as with any event driven solution, I assume.)
- Event generation may never end because of some weird nonbreaking loop created by handler.
- <your items here>
I find this approach to be legit, but I may be missing something very important here. As I never met this - let's call it - exhaustive event dispatching
before, I am in doubt.