Using Domain Events within a .NET Core Microservice

Cesar De la Torre

This blog post is related to the previous blog post named “Domain Events vs. Integration Events in Domain-Driven Design and microservices architectures” : https://devblogs.microsoft.com/cesardelatorre/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/

Since I’m writing content related to “Microservices architecture, Docker containers and .NET Core” (draft available here, at GitHub), I thought it would be good to publish blog posts with partial content so I get feedback to take into account for the final Guide/eBook version.

The code snippets shown below are also available at the eShopOnContainers GitHub repo, however, it is still in our Dev branch. We’ll move it to the Master branch when our next iteration is finalized, soon.

Domain Events

Use domain events to explicitly implement side effects of changes within your domain.

In other words, and using DDD lingo, use domain events to explicitly implement side effects across multiple aggregates. Optionally, for better scalability and less impact in database locks, use eventual consistency between aggregates within the same domain.

What is a Domain Event?

An event is “something that has happened in the past”. A domain event is, logically, something that happened in a particular domain and you wish other parts of the same domain could be aware and react based of that.

An important benefit from domain events is that side effects, after something happened in a domain, can be expressed explicitly instead of implicitly. For example, if you were using just Entity Framework and entities or even aggregates, if there is a change to the side effects of a use case, it will be implicit concept implemented by code after something happened. Sometimes you don’t know if that side effect is part of the main operation or if it is really a side effect. When using domain events, it makes the concept explicit and part of the Ubiquitous Language; in the eShopOnContainers application, for example, creating an order is not just about that order, it updates or even creates a Buyer aggregate originated from the original user, because the user is not a buyer until and after he has bought. If using domain events, we can explicitly express that domain rule based on the ubiquitous language provided by the domain/business experts.

Domain events are partially similar to messaging-style events, with one important difference. With true messaging, queuing and a service bus, a message is fired and always handled asynchronously and communicated across processes and machines. This is useful for integrating multiple bounded-contexts, microservices or even different applications. However, with domain events, you want to raise an event from the domain operation you are currently running but you want any side effects of the domain event to occur within the same domain.

Independently of the chosen implementation, the domain events and their side effects (the actions triggered afterwards that are managed by event-handlers) should occur almost immediately, usually in-process, and within the same domain.

Thus, domain events could be synchronous or asynchronous. Integration events, however, should always be asynchronous.

Domain Events vs. Integration Events

Semantically, domain and integration events are the same thing: notifications about something that just happened. However, their implementation can be different. Domain Events are just messages pushed to a Domain Event Dispatcher, which could be implemented as an in-memory mediator based on an IoC container or any other method.

On the other hand, the purpose of Integration events is to propagate committed transactions and updates to additional sub-systems, whether they are other microservices, bounded-contexts or even external applications. Hence, they should occur only if the entity is successfully persisted, since in many scenarios if this fails, the entire operation effectively never happened.

In addition, and as mentioned, integration events must be based on asynchronous communication between multiple microservices (other bounded-contexts) or even external systems/applications. Thus, under the Event Bus interface needs some infrastructure that allows inter-process and distributed communication between potentially remote services. It can be based on a commercial service bus, queues, a shared database used as a mailbox, or any other distributed and ideally push based messaging system.

Domain Events as a preferred way to trigger side effects across multiple aggregates within the same domain

If executing a command related to one aggregate instance requires additional domain rules to be run on one or more additional aggregates, you should design and implement those side effects to be triggered by domain events.

As shown in the image below, and as one of the most important use cases, a domain event should be used to propagate state changes across multiple aggregates within the same domain model.

image

In the example above, the domain event “OrderStarted” might trigger a Buyer creation (if it doesn’t exist) based on the original user’s data when the user initiates an order. A buyer, in the ordering microservice, will be created based on the original user info from the identity microservice (info provided in the CreateOrderCommand). But the domain event is generated by the Order aggregate when it is created in the first place.

Alternately, you can also have the Aggregate Root subscribed for events raised by members of its Aggregate (child entities). For instance, each OrderItem child entity could be raising an event when the item price is higher than some amount or when the product item amount is too high, then having the aggregate root to receive those events and make any kind of global calculus or aggregation.

It is important to highlight that this event based communication is not implemented directly within the aggregates but you need to implement domain event handlers. Doing so, you could have any number of handlers triggering actions when a domain event happens.

Domain events can also be used to simply trigger an open number of application actions when that event happens. For instance, when the order is started, we might also want publish an “Integration Event” into an Event Bus and finally handled to propagate that info to other microservices or to send an email to the buyer/user saying that the order process has started. That action is not really related to any other aggregate, it is only a simple application action, but since it has to be performed *after* the transaction is committed, it is safer to use an integration event for that which are raised/published to any Event Bus only after the original transaction is performed. Therefore, this is a sample case of “connecting a Domain Event to an Integration Event” and publishing the Integration Event for “external actions” or other microservices. Integration Events and the Event Bus are in a different subject and I might create another blog post about it.

That “open number of actions” to be executed when a domain event happens is the key point. Eventually, the actions and rules in the domain and application will be growing. The complexity or number of actions “when something happens” will be growing and if your code is coupled with “glue”, like just instantiating objects, every time you need to add a new action you will need to change the original code. At that moment, you could be introducing new bugs because with each new requirement you would need to change the original code flow which is going against the Open/Close principle from S.O.L.I.D.. Not only that, the original class that was orchestrating the operations will be growing and growing which is going against the Single Responsibility Principle (SRP).

On the other hand, if you use domain events, you can create a fine-grained and decoupled implementation by segregating responsibilities like in the following approach:

1. Send Command (CreateOrderCommand)

2. Command Handler

o Single Aggregate transaction

o Raise Domain Event (like OrderStarted)

3. Handle Domain Event (within the current process) an open number of side effects in multiple aggregates or application actions

o Verify or create buyer and payment method

o Create and send a related integration event to the Event Bus to propagate states across microservices or trigger external actions like sending an email to the buyer

o Other side effects

As shown in the following image, starting from the same domain event you can handle multiple actions related to other aggregates in the domain or other application actions you might need to perform, like publishing additional Integration Events (different than Domain Events) to propagate or trigger any action in other microservices.

image

The event handlers are typically placed at the application layer as you will be using specific infrastructure objects like Repositories or any infrastructure and application API. From that sense, event handlers are similar to command handlers so both are part of the application layer. The important difference is that a command has to be processed just once. A domain event could be processed cero or ‘n’ times targeting multiple purposes.

Having the possibility of an open number of handlers per domain event would allow you to add many more domain rules without impacting/changing your current code. For instance, adding the following domain/business rule would be as easy as adding one or several new handlers for the following event:

When a customer’s total amount purchased in the store (including any number of orders) exceeds $6,000, apply a 10% off discount to every new order

Implementing Domain Events

How to implement a domain event

In terms of C# code implementation, a domain event is simply a data-holding structure or class, like a DTO (Data Transfer Object) with all the information related to what just happened in the domain, like in the following code.

Regarding the ubiquitous language to be used, since an event is “something that happened in the past” it is very important that the class name of the event must be represented as a verb in the past tense such as OrderStartedDomainEvent,or OrderShippedDomainEvent. For example, the following domain event is how it is implemented in the Ordering microservice at the eShopOnContainers application.

public class OrderStartedDomainEvent : IAsyncNotification

{

public int CardTypeId { get; private set; }

public string CardNumber { get; private set; }

public string CardSecurityNumber { get; private set; }

public string CardHolderName { get; private set; }

public DateTime CardExpiration { get; private set; }

public Order Order { get; private set; }

public OrderStartedDomainEvent(Order order,

int cardTypeId, string cardNumber,

string cardSecurityNumber, string cardHolderName,

DateTime cardExpiration)

{

Order = order;

CardTypeId = cardTypeId;

CardNumber = cardNumber;

CardSecurityNumber = cardSecurityNumber;

CardHolderName = cardHolderName;

CardExpiration = cardExpiration;

}

}

 

Basically, it is a class that holds all the data related to the OrderStarted event.

Events must be immutable. An important characteristic of events is that since an event is something that happened in the past, it shouldn’t change, therefore it must be an immutable class, as you can notice in the previous code where the properties are read only from the outside of the object and the only way to update the object is through the constructor when you actually create the event object.

Raising domain events

The next question you might have is, “ok, this is cool, but how do I raise a domain event so it reaches its related event handlers?”. Well, you could choose between multiple techniques or approaches for that.

Udi Dahan originally proposed in several related posts like Domain Events – Take 2, to use a static class for managing and raising the events, like a static class named DomainEvents which would raise domain events immediately when calling the DomainEvents.Raise(Event myEvent).

Jimmy Bogard also wrote a good post following a similar approach at Strengthening your domain: Domain Events.

However, when the domain events class is static, it also dispatches to handlers immediately. This makes testing and debugging more difficult because the event handlers with the side effects logic will be executed immediately right after raising the event. When you are testing and debugging you would want to focus and run just what’s happening on the current aggregate classes instead of suddenly being redirected to other event handlers running side effects related to other aggregates or application logic. This is why other evolved approaches appeared, as explained in the next section.

The deferred approach for raising and dispatching events

Instead of dispatching to a domain event handler immediately, a better approach is to store/add the domain events in a collection and right after or before committing the transaction (like with SaveChanges() in EF) , dispatch those domain events. That approach was neatly described also by Jimmy Bogard at the “A better domain events pattern post”.

Deciding if you send the domain events right before or after commiting the transaction is very important as depending on that you will include the side effects as part of the same transaction or in different transactions. In the last case, you would need to deal with eventual consistency implementations. This topic is precisely discussed in the next section.

The deferred approach is what the reference application eShopOnContainers uses. First, you add/store the events happening in your entities into a collection or list of events per entity. That list would be part of the entity object, better if coming from your base entity class, as shown in the code below.

 

public abstract class Entity

{

//…

   private List<IAsyncNotification> _domainEvents;

public List<IAsyncNotification> DomainEvents => _domainEvents;

public void AddDomainEvent(IAsyncNotification eventItem)

{

_domainEvents = _domainEvents ?? new List<IAsyncNotification>();

_domainEvents.Add(eventItem);

}

public void RemoveDomainEvent(IAsyncNotification eventItem)

{

if (_domainEvents is null) return;

_domainEvents.Remove(eventItem);

}

//…

}

Thus, whenever you want to raise an event, you would just add it to the event collection like in the following code to be placed within any aggregate entity method.

var orderStartedDomainEvent = new OrderStartedDomainEvent(this, //Order object

cardTypeId, cardNumber,

cardSecurityNumber,

cardHolderName,

cardExpiration);

this.AddDomainEvent(orderStartedDomainEvent);

Notice that the method “AddDomainEvent” the only thing is doing is “adding an event to the list”, nothing more. It is still not reaching the event handler.

Later on, when committing the transaction into the database is when you really want to dispatch the events. If using Entity Framework Core, that means at the “SaveChanges” method level of your EF DbContext, as in the following code.

 

//EF Core DbContext

public class OrderingContext : DbContext, IUnitOfWork

{

//…

public async Task<int> SaveEntitiesAsync()

{

// Dispatch Domain Events collection.

// Choices:

// A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including

// side effects from the domain event handlers which are using the same DbContext with Scope lifetime

// B) Right AFTER committing data (EF SaveChanges) into the DB. will make multiple transactions.

// You will need to handle eventual consistency and compensatory actions in case of failures.

     

      await _mediator.DispatchDomainEventsAsync(this);

 

// After executing this line all the changes (from the Command Handler and Domain Event Handlers)

// performed thought the DbContext will be commited

var result = await base.SaveChangesAsync();

}

}

 

With that code, you dispatch the entity events to their respective event handlers but you decouple the raising of a domain event (a simple add in memory) from dispatching to an event handler.

In addition to that, depending on what kind of dispatcher you are using, you could be dispatching the events synchronously or asynchronously.

Single transaction across aggregates vs. eventual consistency across aggregates

This is an arguable topic. Many DDD authors like Eric Evans, Vaughn Vernon and others defend the rule of “1 Transaction = 1 Aggregate” and therefore, eventual consistency across aggregates, for instance:

E.E. DDD p128: Any rule that spans AGGREGATES will not be expected to be up-to-date at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specific time.

V.V. Effective Aggregate Design. Part II: Making Aggregates Work Together. p9: …Thus, if executing a command on one aggregate instance requires that additional business rules execute on one or more aggregates, use eventual consistency… …There is a practical way to support eventual consistency in a DDD model. An aggregate method publishes a domain event that is in time delivered to one or more asynchronous subscribers.

This rationale is based on embracing fine-grained transactions instead of transactions spanning many aggregates or entities because in the second case the database locks amount will be pretty bad in large scale applications with a high scalability needs. Embracing the fact that high-scalable applications must not have instant transactional consistency between multiple aggregates helps accepting the concept of eventual consistency. Atomic changes are in many cases not needed by the business, and it is in any case responsibility of the domain experts to say that something really needs atomic transactions or not. Then, if some operation always needs an atomic transaction between multiple aggregates, you should at least wonder if your aggregate should be larger and was not correctly designed.

However, other developers and architects, like Jimmy Bogard, are okay by spanning a single transaction across several aggregates but only when those additional aggregates are related to side effects for the same original command, for instance:

J.B. A better domain events pattern: … Typically, I want the side effects of a domain event to occur within the same logical transaction, but not necessarily in the same scope of raising the domain event… … Just before we commit our transaction (DbContext SaveChanges()), we dispatch our events to their respective handlers.

If you are dispatching the domain events right before committing the original transaction is because you want the side effects of those events to be included in the same transaction so, for instance, if the EF DbContext SaveChanges() fails the transaction will roll back all changes, including the rest of the side effect operations implemented by the related domain event handlers because the DbContext life scope is by default defined as “scoped”, so the DbContext object is shared across multiple Repositories objects being instantiated within the same scope or object graph which also coincides with the HttpRequest scope when developing Web API or MVC apps.

In reality, both approaches (single atomic transaction vs. eventual consistency) can be right, it really depends on your domain/business requirements and what the domain experts tell you. Also, depending on how scalable you need it to be (more granular transactions will provoke less impact in regards database locks) and how much investment you are willing to do in your code, since eventual consistency will require a more complex code in order to detect possible inconsistencies across aggregates and the need to implement compensatory actions. Take into account that it you commit changes on the original aggregate in the first place and afterwards, when the events are being dispatched there is any issue and the events handlers cannot commit their side effects, you will have inconsistencies between aggregates.

A way to allow compensatory actions would be to store the domain events into additional database tables so it can be part of the original transaction. Afterwards, you could have batch processing detecting inconsistencies and running compensatory actions in case of issues by comparing the list of events with the current state of the aggregates.

In any case, you can choose the approach you might need, but the initial “deferred approach” implementation for raising and dispatching domain events would be pretty similar.

That is neat, but, how do you actually dispatch those events to their respective event handlers? What is that _mediator object that you see in the previous code? Well, that has to do with the techniques and artifacts you can use to map between events and their event handlers.

The Domain Event Dispatcher: Mapping from events to event handlers

Once you are able to dispatch or publish the events you need any kind of artifact that will publish the event so every related handler would get it and will process side effects based on that event.

One way to do it would be with a real messaging system or even an Event Bus possibly based on a Service Bus. However, that might be too much for processing domain events since you just need to process those events within the same process (same domain and application layer).

One way to map from events to multiple event handlers is by using types registration in an IoC container so you can dynamically infer where to dispatch the events. In other words, you need to know what event handlers need to get any specifc event. You can see a simplified approach for that in the following image.

image

You can build all the “plumbing” and artifacts to implement that approach by yourself, however, you can also use already available libraries like MediatR which underneath uses your IoT container, so you can directly use the pre-defined interfaces and mediator’s publish/dispatch methods.

In terms of code, you first need to register the event handler types in your IoC container.

public class MediatorModule : Autofac.Module

{

protected override void Load(ContainerBuilder builder)

{

// Other registrations

// Register the DomainEventHandler classes (they implement IAsyncNotificationHandler<>) in

// assembly holding the Domain Events

builder.RegisterAssemblyTypes(

typeof(ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler).

      GetTypeInfo().Assembly)

                            .Where(t => t.IsClosedTypeOf(typeof(IAsyncNotificationHandler<>)))

.AsImplementedInterfaces();

// Other registrations

}

}

That code first identifies the assembly holding the domain event handlers based on the assembly that holds any of them. Then, since all the event handlers implement the interface IAsyncNotificationHandler it just searched for those types and registers all the event handlers.

Howe to subscribe to domain events

When using MediatR, each event handler is enforced to use an event type to be provided on the generic’s parameter of the IAsyncNotificationHandler interface, as you can see in the following code.

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler

: IAsyncNotificationHandler<OrderStartedDomainEvent>

Based on that relationship between event and event handler (that can be considered the subscription), the mediator artifact is able to discover all the event handlers per event and trigger each of those event handlers.

How to handle domain events

Finally, the event handler will usually implement application layer code which will be using infrastructure repositories to obtain the required additional aggregates and execute side effect domain logic.

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler

                                                   : IAsyncNotificationHandler<OrderStartedDomainEvent>

{

private readonly ILoggerFactory _logger;

private readonly IBuyerRepository<Buyer> _buyerRepository;

private readonly IIdentityService _identityService;

public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(ILoggerFactory

logger, IBuyerRepository<Buyer> buyerRepository, IIdentityService identityService)

{

//Parameter’s validations

//…

}

public async Task Handle(OrderStartedDomainEvent orderStartedEvent)

{

var cardTypeId = (orderStartedEvent.CardTypeId != 0) ? orderStartedEvent.CardTypeId : 1;

var userGuid = _identityService.GetUserIdentity();

var buyer = await _buyerRepository.FindAsync(userGuid);

bool buyerOriginallyExisted = (buyer == null) ? false : true;

if (!buyerOriginallyExisted)

{

buyer = new Buyer(userGuid);

}

buyer.VerifyOrAddPaymentMethod(cardTypeId,

$”Payment Method on {DateTime.UtcNow}”,

orderStartedEvent.CardNumber,

orderStartedEvent.CardSecurityNumber,

orderStartedEvent.CardHolderName,

orderStartedEvent.CardExpiration,

orderStartedEvent.Order.Id);

var buyerUpdated = buyerOriginallyExisted ? _buyerRepository.Update(buyer) :

      _buyerRepository.Add(buyer);

await _buyerRepository.UnitOfWork.SaveEntitiesAsync();

//Logging code using buyerUpdated info, etc.

}

}

The former event handler’s code is considered application layer as it is using infrastructure repositories explained in the next section focusing on the infrastructure-persistence layer. Event Handlers could also use other infrastructure components.

Domain events could generate Integration events to be published outside of the microservice boundaries

Finally, is important to mention that sometimes you might want to propagate events across multiple microservices. That is considered an integration event and it could be published through an Event Bus from any specific domain event handler.

Conclusions on domain events

As stated, use domain events to explicitly implement side effects of changes within your domain.

In other words, and using DDD lingo, use domain events to explicitly implement side effects across one or multiple aggregates. Additionally, and for better scalability and less impact in database locks, use eventual consistency between aggregates within the same domain.

For additional information on domain events, read the following references.

 

References – Implementing Domain Events

What is a Domain Event? [Greg Young]

http://codebetter.com/gregyoung/2010/04/11/what-is-a-domain-event/

Domain Events [Jan Stenberg]

https://www.infoq.com/news/2015/09/domain-events-consistency

A Better Domain Events Pattern [Jimmy Bogard]

https://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-pattern/

Effective Aggregate Design Part II: Making Aggregates Work Together [Vaughn Vernon]

http://dddcommunity.org/wp-content/uploads/files/pdf_articles/Vernon_2011_2.pdf

Strengthening your domain: Domain Events [Jimmy Bogard]

https://lostechies.com/jimmybogard/2010/04/08/strengthening-your-domain-domain-events/

Domain Events Pattern Example [Tony Truong]

http://www.tonytruong.net/domain-events-pattern-example/

Domain Events – Take 2 [Udi Dahan]

http://udidahan.com/2008/08/25/domain-events-take-2/

Domain Events – Salvation [Udi Dahan]

http://udidahan.com/2009/06/14/domain-events-salvation/

How to create fully encapsulated Domain Models [Udi Dahan]

http://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/

Don’t publish Domain Events, return them! [Jan Kronquist]

https://blog.jayway.com/2013/06/20/dont-publish-domain-events-return-them/

Domain Events vs. Integration Events in DDD and microservices architectures [Cesar de la Torre]

https://devblogs.microsoft.com/cesardelatorre/domain-events-vs-integration-events-in-domain-driven-design-and-microservices-architectures/

 

0 comments

Discussion is closed.

Feedback usabilla icon