Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to publish Domain Event before persisting the Aggregate?

In many different projects I have seen 2 different approaches of raising Domain Events.

  1. 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));
        }
    }
    
  2. 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!

like image 404
DmitriBodiu Avatar asked Apr 16 '17 10:04

DmitriBodiu


People also ask

When to use domain event?

Use domain events to explicitly implement side effects of changes within your domain. In other words, and using DDD terminology, use domain events to explicitly implement side effects across multiple aggregates.

Where can I publish a domain event?

Domain events are published using a simple ApplicationEventPublisher interface. By default, when using ApplicationEventPublisher, events are published and consumed in the same thread.


1 Answers

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

  1. Create metadata from request
  2. Create the factory from the metadata
  3. Pass the factory as an argument.

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.

like image 95
VoiceOfUnreason Avatar answered Oct 03 '22 05:10

VoiceOfUnreason