Strengthening your domain: Domain Events
Previous posts in this series:
- A primer
- Aggregate Construction
- Encapsulated Collections
- Encapsulated Operations
- Double Dispatch Pattern
- Avoiding Setters
In the last post, we rounded out fully encapsulated domain models by encapsulating certain state information that requires certain restrictions in how that information can change. In our model of customers, fees and payments, we started recording fee balance on the Fee object. Once we did this, we started to lock down the public interface of our Fee and Payment objects, so that only operations supported and validated by our domain experts can be executed. The side benefit of this sort of design is that our domain model now strongly reinforces the ubiquitous language.
In an anemic domain model, the ubiquitous language only reflects state information. In our conversations with the domain experts, we are very likely to hear behavioral information as well. We can talk about our model in a more cogent manner now that we have methods like “Customer.ChargeFee” and “Fee.RecordPayment”. Merely having public setters for state information loses the context of why that information changed, and in what context it is valid for that information to change.
As we move towards a fully encapsulated domain model, we may start to encounter a very critical word in our conversations with the domain experts: When. One of the features requested for our Customer object is:
We need to know if a customer has an outstanding balance on more than 3 fees. If a Customer has more than 3 fees with an outstanding balance, we flag them as AtRisk. When a customer pays off a fee and they no longer have more than 3 fees outstanding, they are no longer at risk.
Note that final sentence and the implied interaction. When a fee as been paid off, the customer needs to be notified that to update their AtRisk flag. Whenever we hear the word “When”, that should be a signal to us that there is a potential event in our system. We would like to model this event explicitly in our system and to have our model reflect this kind of relationship between Fee and Customer. In our case, we’re going to use a design from Udi Dahan, Domain Events.
###
Introducing Domain Events
So what are domain events? Domain events are similar to messaging-style eventing, with one key difference. With true messaging and a service bus, a message is fired and handled to asynchronously. With domain events, the response is synchronous. Using domain events is rather straight forward. In our system, we just need to know when a Fee has been paid off. Luckily, our design up to this point has already captured the explicit operation in the Fee object, the RecordPayment method. In this method, we’ll raise an event if the Balance is zero:
public Payment RecordPayment(decimal paymentAmount, IBalanceCalculator balanceCalculator) { var payment = new Payment(paymentAmount, this); _payments.Add(payment); Balance = balanceCalculator.Calculate(this); if (Balance == 0) DomainEvents.Raise(new FeePaidOff(this)); return payment; }
After the Balance has been updated, we raise a domain event using the DomainEvents static class. The name of the event is very explicit and comes from the ubiquitous language, “FeePaidOff”. We include the Fee that was paid off as part of the event message:
public class FeePaidOff : IDomainEvent { public FeePaidOff(Fee fee) { Fee = fee; } public Fee Fee { get; private set; } }
When it comes to testing the events, we have two kinds of tests we want to write. First, we want to write a test that ensures that our Fee raised the correct event:
[Test] public void Should_notify_when_the_balance_is_paid_off() { Fee paidOffFee = null; DomainEvents.Register<FeePaidOff>(e => paidOffFee = e.Fee); var customer = new Customer(); var fee = customer.ChargeFee(100m); fee.RecordPayment(100m, new BalanceCalculator()); paidOffFee.ShouldEqual(fee); }
We’re not checking the Customer “IsAtRisk” flag in this case, as we’re only testing the Fee object in isolation. In an integration test, we’ll have our IoC container in the mix, and we’ll test the handlers as part of the complete operation. Some might argue that the complete model now includes the container, and we want to test the complete operation, events and all in our unit test. I can’t really argue against that, as you might define “unit” to now include the entire domain model, not just one entity.
Handling the event
To handle the event, we want to update the Customer’s AtRisk status for the Customer charged for the paid-off Fee. Our handler then becomes:
public class FeePaidOffHandler : IHandler<FeePaidOff> { private readonly ICustomerRepository _customerRepository; public FeePaidOffHandler(ICustomerRepository customerRepository) { _customerRepository = customerRepository; } public void Handle(FeePaidOff args) { var fee = args.Fee; var customer = _customerRepository.GetCustomerChargedForFee(fee); customer.UpdateAtRiskStatus(); } }
Because our handlers are pulled from the container, we can use normal dependency injection to process the event. If we were going the transaction script route, we would likely see this updating in some sort of application service. With our event handler, we ensure that our model stays consistent. The UpdateAtRiskStatus on Customer now can apply our original business logic:
public bool IsAtRisk { get; private set; } public void UpdateAtRiskStatus() { var totalWithOutstandingBalance = Fees.Count(fee => fee.HasOutstandingBalance()); IsAtRisk = totalWithOutstandingBalance > 3; }
With our domain event in place, we can ensure that our entire domain model stays consistent with the business rules applied, even when we need to notify other aggregate roots in our system when something happens. We’ve also locked down all the ways the risk status could change (charged a new fee), so we can keep our Customer aggregate consistent even in the face of changes in a separate aggregate (Fee).
This pattern isn’t always applicable. If I need to do something like send an email, notify a web service or any other potentially blocking tasks, I should revert back to normal asynchronous messaging. But for synchronous messaging across disconnected aggregates, domain events are a great way to ensure aggregate root consistency across the entire model. The alternative would be transaction script design, where consistency is enforced not by the domain model but by some other (non-intuitive) layer.
Strengthening your domain: conclusion
Creating consistent aggregate root boundaries with a fully encapsulated domain model isn’t that hard, if you know what code smells to look for. You can run into issues with less-mature ORMs that do not truly support a POCO domain model. Having a fully encapsulated domain model represents the entirety of the ubiquitous language: state AND behavior. Anemic domain models only tell half the picture in representing only the state.
Fully encapsulated domain models ensure that my domain model remains self-consistent, and that all invariants are satisfied at the completion of an operation. Anemic domain models do not enforce consistency rules, and rely on reams of services to keep the model valid.
For many, many applications, a domain model is more trouble than it is worth, as the behavior of these services can be quite simple. However, much of DDD is just plain good OOP, and a well-crafted domain model can be realized simply by paying attention to code smells and refactoring.