When do domain event handlers come into play?
Asked Answered
E

1

10

I have a simple examle domain of two aggregate roots and one regular entity. Tenant, UserGroup and User where in this particular sample the Tenant and User make up for the two AggregateRoots.

When a command is received from the UI/Service layer it reaches the command handler which manipulates the write only domain.

You could say that User isn't supposed to be an AggregateRoot at all but since it will be referenced by others, it cannot be a regular entity. (yes?)

These two AggregateRoots need to communicate. A User cannot be created without belonging to a UserGroup, which is an entity in the bounded context of Tenant. Presumably, we can create, since it is a simple constraint, a User through the constructor. User.Create(TenantId, UserGroupId)

It generates a DomainEvent with Date, AggregateVersion and AggregateId (of the user). Now we get to the blurry parts.

Open committing this event into the store, this event is broadcasted onto the bus (memory, whatever). It this the point where domain's event handlers, similar to command handlers, catch the user created and notify/manipulate the Tenant's UserGroup to add the UserId?

Are my thoughts about solving this going into the entirely wrong direction?

Endamoeba answered 6/12, 2011 at 10:48 Comment(4)
You may be able to simplify your problem by reconsidering your model. Domain events are helpful when you require cross bounded context communication, or when you would like supporting business logic, such as email correspondence, to be invoked in response to domain events. Does your model require multiple bounded contexts? Can you contain all required operations in the service layer, such as a UserService?Fiden
One should always check if you cannot model around the problem. In this case however it is a contrived example meant to get answers on how communication between different bounded AR's is supposed to happen.Endamoeba
"What future behavior in UserGroup requires UserId?" is a very important question to ask (otherwise why would you communicate between aggregates). Making Tenant part of your domain seems ... weird if your domain is not about multi tenancy (not to be confused with multi tenancy as a non functional requirement). Use your domain objects to collaborate combined with some TDA if they are in the same BC.Rudolph
It is more about the way the different types of Entities communicate (AR's and non-AR's). But part of it is, as you correctly state, a question about design. In this case, as Dennis Traub mentioned, different AR's communicate with one another through event handlers/Saga's. I have plenty more questions about details though-- I must find someone willing to talk about it first.Endamoeba
D
7

A Saga might be what you are looking for.

Simply put: A saga can be implemented as an event handler that listens for specific events and issues commands to different aggregate roots, or even across context boundaries.

In your case it might look like this:

public class RegisterUserSaga : Handles<UserCreated>
{
    public void Handle<UserCreated>(UserCreated evnt) {
        var tenantId = // you probably know how to find this
        var groupId =  // same here
        var command = new RegisterUserForTenant(evnt.UserId, tenantId, groupId);
        Bus.Send(command);
    }
}

Read more about sagas in this article by Rinat Abdullin or watch "CQRS, race conditions, and sagas - oh my!" by Udi Dahan

Update:

After our extended discussion in the comments I'll try to show how this could work from a different angle (pseudo code ahead). This hopefully sheds some more light on a possible solution:

// Aggregates:

Tenant
    Guid TenantId
    List<Guid> UserGroups

UserGroup
    Guid UserGroupId
    List<Guid> Users

User
    Guid UserId
    Some more details

// Commands:

RequestRegistration(userId, userGroupId, user details)
CreateUser(userId, user details)
AddUserToGroup(userId, userGroupId)

// The initial command would be:

RequestRegistration (leading to a RegistrationRequested event)

// The Saga handles the RegistrationRequested and all subsequent events

UserRegistrationSaga 
    Handle(RegistrationRequested)
    -> send CreateUser command (which eventually leads to a UserCreated event)
    Handle(UserCreated)
    -> send AddUserToGroup command (-> UserAddedToGroup event)
    Handle(UserAddedToGroup)
    -> Done
Devisable answered 6/12, 2011 at 14:21 Comment(13)
If possible, can you shed some light if I have the correct sequence in mind? Change AR, commit to ES, broadcast events, Saga picks up and changes other AR's and so forth? And this would need some mechanism to wake, instanciate and periodically call Saga's, yes?Endamoeba
This is exactly the sequence I had in mind. A basic stateless saga implementation could simply be registered at start-up like any other event handler. A more sophisticated implementation might include persistence. For starters a simple stateless implementation should be sufficient to get used to the concept. For more complex variants have a look at the sagas in NServiceBus and Jonathan Oliver's Common Domain/Event Store at GitHub.Devisable
In this particular case, since the UserCreated event might not immediately picked up by the Saga we are dealing with Eventual Consistancy in the domain, yes? Since a user might have been 'added' to a Tenant's group but the tenant AR isn't updated yet?Endamoeba
Yes. In DDD immediate consistency (aka transactions) should only be enforced inside a single aggregate's boundaries. Every behaviour that affects multiple aggregates or even crosses context boundaries is inherently considered to be eventually consistent.Devisable
Yet, in my contrived example, is the Saga (or event!) allowed to refer to the UserGroupId? I understood you can only refer to them by AR Id... or?Endamoeba
That depends on your domain model. If UserGroup is not an aggregate root, the saga should not be able to directly refer to it, as you correctly assume. But this is more of a modelling issue. Since I don't know the specifics of your domain, I can't tell if it might make sense to expose UserGroup as an AR. My code sample may be flawed based on the assumptions of your domain, but it was meant to clarify the concept behind the saga.Devisable
Yes, your answer is certainly half my question's answer. But the other half is attempting to understand how to solve the situation where, as in this contrived example, you must add/change something for an Entity in the bounded context of another AR?Endamoeba
I'm not sure if I correctly understand. I assume you are aware of the difference between an aggregate and a bounded context? They are not the same. One bounded context can consist of many different aggregates, and an aggregate's contents can only be accessed through the aggregate root. Maybe in your case the UserGroup is an aggregate in itself, refering to User and being referenced by Tenant. And all three are part of a BC that could be called Identity and Access Context.Devisable
I updated the original answer, hope this helps a little more.Devisable
The altered reply clarifies much. The RequestRegistration is handled by a command handler. But I don't see what AR is will do a Register on to actually generate a RegistrationRequested event. All DomainEvents are associated with an AR, right?Endamoeba
An domain event can be published even without an AR. Especially when there is no business logic involved, you can fall back to e.g. a transaction script in your command handler, that just publishes the event without needing an AR. Basically on the write side the main purpose of the domain model is to implement business logic. If there is no logic, then there is no need for an object that enforces it. In your case though, the associated business logic might live (and thus should be enforced) in the User AR.Devisable
I accepted your answer. In your example however, do you image that the command handler for RequestRegistration immediately fires the RegistrationRequested to kickstart the saga or did you envision something else?Endamoeba
See my previous comment. If there is only simple validation like field or uniqueness checks involved, the event can be published from within the command handler.Devisable

© 2022 - 2024 — McMap. All rights reserved.