Reputation: 1210
In many different projects I have seen 2 different approaches of raising Domain Events.
Raise Domain Event directly from aggregate. For example imagine you have Customer aggregate and here is a method inside it:
public virtual void ChangeEmail(string email)
{
if(this.Email != email)
{
this.Email = email;
DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email));
}
}
I can see 2 problems with this approach. The first one is that the event is raised regardless of whether the aggregate is persisted or not. Imagine if you want to send an email to a customer after successful registration. An event "CustomerChangedEmail" will be raised and some IEmailSender will send the email even if the aggregate wasn't saved. The second problem with the current implementation is that every event should be immutable. So the question is how can I initialize its "OccuredOn" property? Only inside aggregate! Its logical, right! It forces me to pass ISystemClock (system time abstraction) to each and every method on aggregate! Whaaat??? Don't you find this design brittle and cumbersome? Here is what we'll come up with:
public virtual void ChangeEmail(string email, ISystemClock systemClock)
{
if(this.Email != email)
{
this.Email = email;
DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email, systemClock.DateTimeNow));
}
}
The second approach is to go what Event Sourcing pattern recommends to do. On each and every aggregate, we define a (List) list of uncommited events. Please payAttention that UncommitedEvent is not a domain Event! It doesn't even has OccuredOn property. Now, when ChangeEmail method is called on Customer Aggregate, we don't raise anything. We just save the event to uncommitedEvents collection which exists on our aggregate. Like this:
public virtual void ChangeEmail(string email)
{
if(this.Email != email)
{
this.Email = email;
UncommitedEvents.Add(new CustomerChangedEmail(email));
}
}
So, when does the actual domain event is raised??? This responsibility is delegated to persistence layer. In ICustomerRepository we have access to ISystemClock, because we can easily inject it inside repository. Inside Save() method of ICustomerRepository we should extract all uncommitedEvents from Aggregate and for each of them create a DomainEvent. Then we set up OccuredOn property on newly created Domain Event. Then, IN ONE TRANSACTION we save the aggregate and publish ALL domain events. This way we'll be sure that all events will will raised in transnational boundary with aggregate persistence.
What I don't like about this approach? I don't want to create 2 different types for the same event, i.e for CustomerChangedEmail behavior I should have CustomerChangedEmailUncommited type and CustomerChangedEmailDomainEvent. It would be nice to have just one type. Please share your experience regarding to this topic!
Upvotes: 19
Views: 6804
Reputation: 11408
The way I would solve the sending of email problem is by decoupling the publishing of the event and the handling of the event through a messaging queue. This way you close the transaction after sending the event to the queue, and the sending of email, or other effects that cannot or should not be part of the original DB transaction, will happen shortly after, in a different transaction. The simplest way to do that, of course, is to have an event handler that publishes domain events onto the queue.
If you want to be extra sure that the domain events will be published to the queue when the transaction is committed, you can save the events to an OUTBOX table that will be committed with the transaction, and then have a thread read from the table and publish to the event queue
Upvotes: 2
Reputation: 394
I tend to implement domain events using the second approach.
Instead of manually retrieving and then dispatching all events in the aggregate roots repository I have a simple DomainEventDispatcher
(application layer) class which listens to various persistence events in the application. When an entity is added, updated or deleted it determines whether it is an AggregateRoot
. If so, it calls releaseEvents()
which returns a collection of domain events that then get dispatched using the application EventBus
.
I don't know why you are focusing so much on the occurredOn
property.
The domain layer is only concerned with the meat of the domain events such as aggregate root IDs, entity IDs and value object data.
At the application layer you can have an event envelope which can wrap any serialized domain event while giving it some meta data such as a unique ID (UUID/GUID), what aggregate root it came from, the time it occurred etc. This can be persisted to a database.
This meta data is useful in the application layer because you might be publishing these events to other applications using a message bus/event stream over HTTP and it allows each event to be uniquely identifiable.
Again, this meta data about the event generally makes no sense in the domain layer, only the application layer. The domain layer does not care or have any use for event IDs or the time they occurred but other applications which consume these events do. That's why this data is attached at the application layer.
Upvotes: 1
Reputation: 57279
I have seen 2 different approaches of raising Domain Events.
Historically, there have been two different approaches. Evans didn't include domain events when describing the tactical patterns of domain-driven-design; they came later.
In one approach, Domain Events act as a coordination mechanism within a transaction. Udi Dahan wrote a number of posts describing this pattern, coming to the conclusion:
Please be aware that the above code will be run on the same thread within the same transaction as the regular domain work so you should avoid performing any blocking activities, like using SMTP or web services.
event-sourcing, the common alternative, is actually a very different animal, in so far as the events are written to the book of record, rather than merely being used to coordinate activities in the write model.
The second problem with the current implementation is that every event should be immutable. So the question is how can I initialize its "OccuredOn" property? Only inside aggregate! Its logical, right! It forces me to pass ISystemClock (system time abstraction) to each and every method on aggregate!
Of course - see John Carmack's plan files
If you don't consider time an input value, think about it until you do - it is an important concept
In practice, there are actually two important time concepts to consider. If time is part of your domain model, then it's an input.
If time is just meta data that you are trying to preserve, then the aggregate doesn't necessarily need to know about it -- you can attach the meta data to the event elsewhere. One answer, for example, would be to use an instance of a factory to create the events, with the factory itself responsible for attaching the meta data (including the time).
How can it be achieved? An example of a code sample would help me a lot.
The most straight forward example is to pass the factory as an argument to the method.
public virtual void ChangeEmail(string email, EventFactory factory)
{
if(this.Email != email)
{
this.Email = email;
UncommitedEvents.Add(factory.createCustomerChangedEmail(email));
}
}
And the flow in the application layer looks something like
Then, IN ONE TRANSACTION we save the aggregate and publish ALL domain events. This way we'll be sure that all events will will raised in transnational boundary with aggregate persistence.
As a rule, most people are trying to avoid two phase commit where possible.
Consequently, publish isn't usually part of the transaction, but held separately. See Greg Young's talk on Polyglot Data. The primary flow is that subscribers pull events from the book of record. In that design, the push model is a latency optimization.
Upvotes: 1
Reputation: 13256
I am not a proponent of either of the two techniques you present :)
Nowadays I favour returning an event or response object from the domain:
public CustomerChangedEmail ChangeEmail(string email)
{
if(this.Email.Equals(email))
{
throw new DomainException("Cannot change e-mail since it is the same.");
}
return On(new CustomerChangedEmail { EMail = email});
}
public CustomerChangedEmail On(CustomerChangedEmail customerChangedEmail)
{
// guard against a null instance
this.EMail = customerChangedEmail.EMail;
return customerChangedEmail;
}
In this way I don't need to keep track of my uncommitted events and I don't rely on a global infrastructure class such as DomainEvents
. The application layer controls transactions and persistence in the same way it would without ES.
As for coordinating the publishing/saving: usually another layer of indirection helps. I must mention that I regard ES events as different from system events. System events being those between bounded contexts. A messaging infrastructure would rely on system events as these would usually convey more information than a domain event.
Usually when coordinating things such as sending of e-mails one would make use of a process manager or some other entity to carry state. You could carry this on your Customer
with some DateEMailChangedSent
and if null then sending is required.
The steps are:
SendEMailChangedCommand
message (2)There are a couple of ways to do that message sending part that may include it in the same transaction (no 2PC) but let's ignore that for now.
Assuming that previously we had sent an e-mail our DateEMailChangedSent
has a value before we start we may run into the following exceptions:
(1) If we cannot save the event stream then here's no problem since the exception will rollback the transaction and the processing would occur again.
(2) If we cannot send the message due to some messaging failure then there's no problem since the rollback will set everything back to before we started.
(3) Well, we've sent our message so an exception on commit may seem like an issue but remember that we could not set our DateEMailChangedSent
back to null
to indicate that we require a new e-mail to be sent.
The message handler for the SendEMailChangedCommand
would check the DateEMailChangedSent
and if not null
it would simply return, acknowledging the message and it disappears. However, if it is null then it would send the mail either interacting with the e-mail gateway directly ot making use of some infrastructure service endpoint through messaging (I'd prefer that).
Well, that's my take on it anyway :)
Upvotes: 5