In DDD, we establish bounded contexts and define how they will exchange information, which often occurs by raising events. According to Eric Evans, you should:
“Describe the points of contact between the models, outlining explicit translation for any communication and highlighting any sharing.”
These contexts will often communicate as part of a same database transaction. That's actually desired, since it will assure integrity between the whole process. However, in the real world, things can be tricky. Some actions might depend on committed transactions in order to get triggered. This happens, for instance, to services that run outside of the ongoing database transaction. You don’t want to send an email to notify the user that something changed and have the whole operation rolled back seconds later. Same thing happens when our applications consume distributed services that do not support distributed transactions.
What We Want To Achieve
Let's see our main goals. There are many good ways to implement domain event handlers. This implementation will use a dependency container to control the lifetime of the events raiser, it will register events with the container on a very transparent way and support delayed execution. Let's review:
Effortless Registration
In order to create a new handler for an event, we don't want to look for a distant class in another project that has hundreds of event-to-handler bindings and add new lines. To that end, we might want to consider to use a dependency container.
Deferred Execution Support
Handlers that invoke code that are not bound to a transaction should be fired only once the transaction is committed.
Practical Example : Paying an Order on an E-Commerce App
Let's take as example the payment of an order on an e-commerce application. Once the payment is processed, it raises an event. Then, the bounded context that processes customers orders handles the event and places the order for shipment. Furthermore, the bounded context that manages the inventory handles the same event to subtract from the stock the items that were ordered.
![Image 1](/KB/architecture/1176046/Domain-Events-01.png)
Created with Balsamiq Mockups.
Firing the Event
PaymentService
is instantiated by the dependency container. An event is raised on the last line of the PayOrder
method.
public class PaymentService : IPaymentService
{
private readonly IDomainEventsRaiser _events;
private readonly IRepository<Order> _orderRepository;
private readonly IRepository<Payment> _paymentRepository;
public PaymentService(IRepository<Payment> paymentRepository,
IRepository<Order> orderRepository, IDomainEventsRaiser events)
{
_paymentRepository = paymentRepository;
_orderRepository = orderRepository;
_events = events;
}
public void PayOrder(int orderId, decimal amount)
{
Order order = _orderRepository.GetById(orderId);
Payment payment = new Payment()
{
OrderId = orderId,
Amount = amount
};
_paymentRepository.Insert(payment);
_events.Raise(new OrderPaidEvent(payment, order.Items));
}
}
IDomainEventsRaiser
has the responsibility of raising events and it needs only one method to do so:
public interface IDomainEventsRaiser
{
void Raise<T>(T domainEvent) where T : IDomainEvent;
}
What makes OrderPaidEvent
an event is the fact that it implements the IDomainEvent
interface. This interface
actually does not expose any service. Its only purpose is to serve as a generic constraint to the Raise
method of IDomainEventsRaiser
to group classes that represent an event.
public class OrderPaidEvent : IDomainEvent
{
public OrderPaidEvent(Payment payment, IEnumerable<OrderItem> orderItems)
{
Payment = payment;
OrderItems = new List<OrderItem>(orderItems);
}
public Payment Payment { get; private set; }
public List<OrderItem> OrderItems { get; set; }
}
Handling the Event
As we said earlier, we want effortless registration. To that end, we will use some conventions. Every handler will have to implement the interface IHandles
, as described below:
public interface IHandles<T> where T : IDomainEvent
{
void Handle(T domainEvent);
}
Once the order is paid, its related items are subtracted from the stock. In other words, once OrderPaidEvent
is raised, a SubtractInventaryWhenOrderPaidEventHandler
will handle it. The IHandles
interface makes the bridge between a domain event and its handlers.
public class SubtractInventaryWhenOrderPaidEventHandler : IHandles<OrderPaidEvent>
{
private readonly IInventoryService _inventory;
public SubtractInventaryWhenOrderPaidEventHandler(IInventoryService inventory)
{
_inventory = inventory;
}
public void Handle(OrderPaidEvent domainEvent)
{
foreach (var item in domainEvent.OrderItems)
_inventory.SubtractAvailability(item.InventoryItemId, item.Quantity);
}
}
The handler is also created by the dependency container. That allows dependencies to be passed more easily.
We can have multiple handlers for the same event. That said, once the order is paid, it can be placed for delivery. That means that a PlaceOrderWhenPaidEventHandler
will handle this same event (we can also have one single handler that handles multiples events).
public class PlaceOrderWhenPaidEventHandler : IHandles<OrderPaidEvent>
{
private readonly IOrderPlacementService _orderPlacement;
public PlaceOrderWhenPaidEventHandler(IOrderPlacementService orderPlacement)
{
_orderPlacement = orderPlacement;
}
public void Handle(OrderPaidEvent domainEvent)
{
_orderPlacement.PlaceOrder(domainEvent.Payment.OrderId);
}
}
The Domain Events Raiser
Now that we understand how events are fired and handled, let's see how the events raiser is implemented:
class DomainEventsRaiser : IDomainEventsRaiser
{
private readonly IServiceProvider _locator;
internal DomainEventsRaiser(IServiceProvider locator)
{
_locator = locator;
}
public void Raise<T>(T domainEvent) where T : IDomainEvent
{
IHandles<T>[] allHandlers = (IHandles<T>[])_locator.GetService(typeof(IHandles<T>[]));
if (allHandlers != null && allHandlers.Length > 0)
foreach (var handler in allHandlers)
handler.Handle(domainEvent);
}
}
As we can see, a locator finds all the handlers that handle a specific event, then fires each handler sequentially. The domain events raiser is also created by the dependency container. It allows services to have its reference on their constructors.
These same principles could be used for other needs, like CQRS to handle commands.
Convention-Based Registration
You might want to use convention-based registration to services that are a part of the same concept, have similar structure and follow a same pattern of registration and resolving. A more "manual" registration can be more interesting for services that don't fall on the first rule and require more specific needs.
We will be using Unity on this section, but any decent container should give you similar capabilities.
Container.RegisterTypes(new EventHandlersConvention());
This will make the container register all classes that implement IHandles<T>
. That allows the DomainEventsRaiser
to use a locator that find the handlers for a specific event.
class EventHandlersConvention : RegistrationConvention
{
public override Func<Type, IEnumerable<Type>> GetFromTypes()
{
return WithMappings.FromAllInterfaces;
}
public override Func<Type, IEnumerable<InjectionMember>> GetInjectionMembers()
{
return (t) => new InjectionMember[0];
}
public override Func<Type, LifetimeManager> GetLifetimeManager()
{
return WithLifetime.Transient;
}
public override Func<Type, string> GetName()
{
return WithName.TypeName;
}
public override IEnumerable<Type> GetTypes()
{
Type handlerType = typeof(IHandles<>);
return AllClasses.FromLoadedAssemblies(skipOnError: false).
Where(t => !t.IsAbstract &&
t.GetInterfaces().Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition().Equals(handlerType)));
}
}
Important: The lifetime of DomainEventsRaiser
is not transient. It can be singleton, but I suggest scoped to the duration of a full transaction, since the state of this object (the events queue that we will see on the next section) is not reusable between one transaction and another.
Adding Deferred Execution Support
We already have a full working example of domain events that are registered using conventions. Now let's add a teaspoon of real life.
Let's say, once the order is paid, an e-mail is sent to the customer to inform that his payment has been processed. This is what our process now looks like:
![Image 2](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
Created with Balsamiq Mockups.
We will be handling e-mail sending using the domain events raiser that we proposed for domain events. However, the e-mail should be sent only after the transaction is committed.
So, in our IHandles interface
, we add a Deferred
property:
public interface IHandles<T> where T : IDomainEvent
{
void Handle(T domainEvent);
bool Deferred { get; }
}
One single class can handle many events. NotificationsHandler
demonstrates how this behavior could be achieved:
public class NotificationsHandler : IHandles<OrderPaidEvent>,
IHandles<OrderShippedEvent>
{
private readonly IUserNotifier _notifier;
public NotificationsHandler
(IUserNotifier notifier)
{
_notifier = notifier;
}
public bool Deferred { get { return true; } }
public void Handle(OrderPaidEvent domainEvent)
{
_notifier.Notify("Yay! Your payment has been processed.");
}
public void Handle(OrderShippedEvent domainEvent)
{
_notifier.Notify(string.Format("Your order has finally been shipped.
Address : \"{0}\"", domainEvent.Order.ShipmentAddress));
}
}
DeferredDomainEventsRaiser
And DeferredDomainEventsRaiser
replaces the simple DomainEventsRaiser
that I mentioned earlier. It adds deferred handlers in a Queue and dispatches them once the transaction is committed. Please notice that:
- Not all events are deferred. Only those that have
IHandles.Deferred = true
. - In addition, events are never deferred if there is no transaction in progress.
class DeferredDomainEventsRaiser : IDomainEventsRaiser
{
private readonly IServiceProvider _resolver;
private readonly ConcurrentQueue<Action> _pendingHandlers = new ConcurrentQueue<Action>();
private readonly IDbStateTracker _dbState;
public DeferredDomainEventsRaiser(IServiceProvider resolver, IDbStateTracker dbState)
{
_resolver = resolver;
_dbState = dbState;
_dbState.TransactionComplete += this.Flush;
_dbState.Disposing += this.FlushOrClear;
}
public void Raise<T>(T domainEvent) where T : IDomainEvent
{
IHandles<T>[] allHandlers =
(IHandles<T>[])_resolver.GetService(typeof(IHandles<T>[]));
if (allHandlers != null && allHandlers.Length > 0)
{
IHandles<T>[] handlersToEnqueue = null;
IHandles<T>[] handlersToFire = allHandlers;
if (_dbState.HasPendingChanges())
{
handlersToEnqueue = allHandlers.Where(h => h.Deferred).ToArray();
if (handlersToEnqueue.Length > 0)
{
lock (_pendingHandlers)
foreach (var handler in handlersToEnqueue)
_pendingHandlers.Enqueue(() => handler.Handle(domainEvent));
handlersToFire = allHandlers.Except(handlersToEnqueue).ToArray();
}
}
foreach (var handler in handlersToFire)
handler.Handle(domainEvent);
}
}
private void Flush()
{
Action dispatch;
lock (_pendingHandlers)
while (_pendingHandlers.TryDequeue(out dispatch))
dispatch();
}
private void FlushOrClear()
{
if (!_dbState.HasPendingChanges())
Flush();
else
{
Clear();
}
}
private void Clear()
{
Action dispatch;
lock (_pendingHandlers)
while (_pendingHandlers.TryDequeue(out dispatch)) ;
}
}
This new interface IDbStateTracker
simply exposes some events about the state of the database and it could be implemented by a UnitOfWork
(see the source code attached to this post, also available on GitHub).
public interface IDbStateTracker : IDisposable
{
event Action Disposing;
event Action TransactionComplete;
bool HasPendingChanges();
}
Putting It All Together
This approach allows event handlers to be registered automatically by using conventions. Also, the same design allows event handlers to be fired online or have their execution delayed until the transaction is over. The source code attached to this article (also available on GitHub) contains a full working example of the principles discussed here.
Software architect, Machine learning engineer, full stack cloud developer, MSc in Machine Learning, Microsoft Certified Solutions Developer (MCSD).