The previous tip talked about domain events that fire before persistence. This week we'll look at another kind of domain event that should typically only fire after persistence.
The previous tip talked about domain events that fire before persistence. This week we'll look at another kind of domain event that should typically only fire after persistence.
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
If you're new to the domain events pattern, I recommend you listen to episode 22 before this one. In general, I recommend listening to this podcast in order, but I can't force that on you...
When you have a scenario in your application where a requirement is phrased "when X happens, then Y should happen," that's often an indication that using a domain event might be appropriate. If the follow-on behavior has side effects that extend beyond your application, that's often an indication that the event shouldn't occur unless persistence is successful. Let's consider a contrived real-world example.
Imagine you have a simple ecommerce application. People can browse products, add them to a virtual cart or basket, and checkout by providing payment and shipping details. Everything is working fine when you get a new requirement: when the customer checks out, they should get an email confirming their purchase. Sounds like a good candidate for a domain event, right? Ok, so your first pass at this is to simply go into the Checkout method and raise a CartCheckedOut event, which you then handle with a NotifyCustomerOnCheckoutHandler class. You're using a simple in-proc approach to domain events so when you raise an event, all handlers fire immediately before execution resumes. You roll out the change with the next deployment. Unfortunately, another change to the codebase resulted in an undetected error related to saving new orders. Meaning, they're not being saved in the database. Now the result is that customers are checking out, being redirected to a friendly error screen, but also getting an email now confirming their order was placed. They're mostly assuming everything is fine on account of the pleasant email confirmation, but in fact your system has no record of the order they just placed because it didn't save. In this kind of situation, you'd really rather not send that confirmation email until you've successfully saved the new order.
While in-proc domain events are often implemented using simple static method calls to raise or register for events, post-persistence events need to be stored somewhere and only dispatched once persistence has been successful. One approach you can use for this in .NET applications is to store the events in a collection on the entity or aggregate root, and then override the behavior of the Entity Framework DbContext so that it dispatches these events once it has successfully saved the entity or aggregate. My CleanArchitecture sample on GitHub demonstrates how to put this approach into action using a technique Jimmy Bogard wrote about a few years ago. It involves overriding the SaveChanges method on the DbContext, finding all tracked entities with events in their collection, and then dispatching these events. His original approach fires the events before actually saving the entity, but I much prefer persisting first and using a different kind of domain event for immediate, no side effect events.
In the Clean Architecture sample, I have a simple ToDo entity that raises an event when it is marked complete. This event is only fired once the entity's state is saved. At that point, a handler tasked with notifying anybody subscribed to that entity's status could safely send out notifications. The pattern is very effective as a lightweight way to decouple follow-on behavior from actions that trigger them within the domain model, and it doesn't require adding additional architecture in the form of message queues or buses to achieve it.
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.