Domain Events - Before Peristence
Domain Events are a DDD design pattern that in my experience can really improve the design of complex applications. In this episode I describe specifically how you would benefit from raising and handling these events prior to persisting the state of your entities.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
So before we get started, let's describe what a domain event is. A domain event is something that happens in your system that a domain expert cares about. Domain events are part of your domain model. They belong in the Core of your Clean Architecture. They should be designed at the abstraction level of your domain model, and shouldn't reference UI or infrastructure concerns.
Domain events are a pattern, and one with several difference implementation approaches. I generally segment these approaches into two camps: before persistence and after persistence. For this tip, we're going to focus on events that occur and are handled prior to persistence. In future tips, I'll talk about domain events that should only be dispatched once persistence has been successful.
So, as a pattern, what problem are domain events designed to solve? Just as with other event-driven programming models, such as user interface events, domain events provide a way to decouple things that occur in your system from things such occurrences trigger. A common example I use is checking out a shopping cart from an ecommerce site. When the user checks out, a variety of other things generally should take place. The order should be saved. Payment should be processed. Inventory should be checked. Notifications should be sent. Now, you could put all of this logic into a Checkout method, but then that method is going to be pretty large and all-knowing. It's probably going to violate the Single Responsibility and Open/Closed Principles. Another approach would be to raise an event when the user checks out, and then to have separate handlers responsible for payment processing, inventory monitoring, notifications, etc.
Looking specifically at events that make sense to handle before persistence, the primary rule is that such events shouldn't have any side effects external to the application. A common scenario is to perform some kind of validation. Imagine you have a Purchase Order domain object, which includes a collection of Line Item objects. The Purchase Order has a maximum amount associated with it, and the total of all the Line Item object amounts must not exceed this amount. For the sake of simplicity let's say the Purchase Order object includes the logic to check whether its child Line Item objects exceeds its maximum. When a Line Item object is updated, how can we run this code?
One option would be to provide a reference from each Line Item to its parent Purchase Order. This is fairly common but results in circular dependencies since Purchase Order also has a reference to a collection of its Line Item objects. These objects together can be considered an Aggregate, and ideally dependencies should flow from the Aggregate Root (in this case Purchase Order) to its children, and not the other way around. So, let's assume we follow this practice, which means we can simply call a method on Purchase Order from Line Item directly.
Another common approach I see developers use instead of domain events is to pull all of the logic up from child objects into the aggregate root object. So instead of having a property setter or property on Line Item to update its amount, there might be a method on Purchase Order called UpdateLineItemAmount that would do the work. This breaks encapsulation and will cause the root object to become bloated while the child objects become mere DTOs. It can work, but it's not very good from an object-oriented design standpoint.
So how would domain events apply here? First, you'd put the logic to modify Line Item on the Line Item class where it belongs. Then, in the UpdateAmount method, you would raise an appropriate domain event, like LineItemAmountUpdated. The aggregate root would subscribe to this event and would handled the event in process (not asynchronously or in another thread). If necessary, the handler could raise an exception. In any case, it could update properties of the root object to indicate whether it was currently in a valid state, which could easily be reflected in the UI. This is one particular use case for domain events that I've found very helpful, and which I typically refer to as aggregate events since there isn't a separate event handler type in this case. I have a small sample showing them in action on GitHub you can check out in the show notes. With aggregate events in place, you can check all the blocks for your object design. Your aggregate's dependencies flow from root to children. Your aggregate's child objects are responsible for their own behavior. Changes to child objects are communicated to the aggregate root which is responsible for ensuring the validity of the entire aggregate. Many scenarios have rules where the root object has to perform validation checks that are based on the aggregate of several child objects, and this pattern provides an elegant solution to this problem.
Would your team or application benefit from an application assessment, highlighting potential problem areas and identifying a path toward better maintainability? Contact me at ardalis.com and let's see how I can help.