Integrating Aggregates
In Chapter 6, we discussed that one of the ways aggregates communicate with the rest of the system is by publishing domain events. External components can subscribe to these domain events and execute their logic. But how are domain events published to a message bus?
Before we get to the solution, let’s examine a few common mistakes in the event pub‐ lishing process and the consequences of each approach. Consider the following code:
01 public class Campaign
02 {
03 |
| ... |
04 |
| List<DomainEvent> _events; |
05 |
| IMessageBus _messageBus; |
06 |
| ... |
07 |
|
|
08 |
| public void Deactivate(string reason) |
09 |
| { |
10 |
| for (l in _locations.Values()) |
11 |
| { |
12 |
| l.Deactivate(); |
13 |
| } |
14 |
|
|
15 |
| IsActive = false; |
16 |
|
|
17 |
| var newEvent = new CampaignDeactivated(_id, reason); |
18 |
| _events.Append(newEvent); |
19 |
| _messageBus.Publish(newEvent); |
20 |
| } |
21 | } |
|
On line 17, a new event is instantiated. On the following two lines, it is appended to the aggregate’s internal list of domain events (line 18), and the event is published to the message bus (line 19). This implementation of publishing domain events is simple but wrong. Publishing the domain event right from the aggregate is bad for two rea‐ sons. First, the event will be dispatched before the aggregate’s new state is committed to the database. A subscriber may receive the notification that the campaign was deactivated, but it would contradict the campaign’s state. Second, what if the database transaction fails to commit because of a race condition, subsequent aggregate logic rendering the operation invalid, or simply a technical issue in the database? Even though the database transaction is rolled back, the event is already published and pushed to subscribers, and there is no way to retract it.
Let’s try something else:
01 public class ManagementAPI
02 {
03 ...
04 private readonly IMessageBus _messageBus;
05 private readonly ICampaignRepository _repository; 06 ...
07 public ExecutionResult DeactivateCampaign(CampaignId id, string reason) 08 {
09 try
10 {
11 var campaign = repository.Load(id);
12 campaign.Deactivate(reason);
13 _repository.CommitChanges(campaign);
14
15 var events = campaign.GetUnpublishedEvents();
16 for (IDomainEvent e in events)
17 {
18 _messageBus.publish(e);
19 }
20 campaign.ClearUnpublishedEvents();
21 }
22 catch(Exception ex)
23 {
24 ...
25 }
26 }
27 }
In the preceding listing, the responsibility of publishing new domain events is shifted to the application layer. On lines 11 through 13, the relevant instance of the Campaign aggregate is loaded, its Deactivate command is executed, and only after the updated state is successfully committed to the database, on lines 15 through 20, are the new domain events published to the message bus. Can we trust this code? No.
In this case, the process running the logic for some reason fails to publish the domain events. Perhaps the message bus is down. Or the server running the code fails right after committing the database transaction, but before publishing the events the sys‐ tem will still end in an inconsistent state, which means that the database transaction is committed, but the domain events will never be published.
These edge cases can be addressed using the outbox pattern.