Event-Sourced Domain Model
The original domain model maintains a state representation of its aggregates and emits select domain events. The event-sourced domain model uses domain events exclusively for modeling the aggregates’ lifecycles. All changes to an aggregate’s state have to be expressed as domain events.
Each operation on an event-sourced aggregate follows this script:
• Load the aggregate’s domain events.
• Reconstitute a state representation—project the events into a state representation that can be used to make business decisions.
• Execute the aggregate’s command to execute the business logic, and consequently, produce new domain events.
• Commit the new domain events to the event store.
Going back to the example of the Ticket aggregate from Chapter 6, let’s see how it would be implemented as an event-sourced aggregate.
The application service follows the script described earlier: it loads the relevant tick‐ et’s events, rehydrates the aggregate instance, calls the relevant command, and persists changes back to the database:
01 public class TicketAPI
02 {
03 private ITicketsRepository _ticketsRepository; 04 ...
05
06 public void RequestEscalation(TicketId id, EscalationReason reason) 07 {
08 |
| var events = _ticketsRepository.LoadEvents(id); |
09 |
| var ticket = new Ticket(events); |
10 |
| var originalVersion = ticket.Version; |
11 |
| var cmd = new RequestEscalation(reason); |
12 |
| ticket.Execute(cmd); |
13 |
| _ticketsRepository.CommitChanges(ticket, originalVersion); |
14 | } |
|
15
16 ...
17 }
The Ticket aggregate’s rehydration logic in the constructor (lines 27 through 31) instantiates an instance of the state projector class, TicketState, and sequentially calls its AppendEvent method for each of the ticket’s events:
18 public class Ticket
19 {
20 ...
21 private List<DomainEvent> _domainEvents = new List<DomainEvent>();
22 private TicketState _state;
23 ...
24
25 public Ticket(IEnumerable<IDomainEvents> events)
26 {
27 _state = new TicketState();
28 foreach (var e in events)
29 {
30 AppendEvent(e);
31 }
32 }
The AppendEvent passes the incoming events to the TicketState projection logic, thus generating the in-memory representation of the ticket’s current state:
33 private void AppendEvent(IDomainEvent @event)
34 {
35 _domainEvents.Append(@event);
36 // Dynamically call the correct overload of the "Apply" method.
37 ((dynamic)state).Apply((dynamic)@event);
38 }
Contrary to the implementation we saw in the previous chapter, the event-sourced aggregate’s RequestEscalation method doesn’t explicitly set the IsEscalated flag to true. Instead, it instantiates the appropriate event and passes it to the AppendEvent method (lines 43 and 44):
39 public void Execute(RequestEscalation cmd)
40 {
41 if (!_state.IsEscalated && _state.RemainingTimePercentage <= 0)
42 {
43 var escalatedEvent = new TicketEscalated(_id, cmd.Reason);
44 AppendEvent(escalatedEvent);
45 }
46 }
47
48 ...
49 }
All events added to the aggregate’s events collection are passed to the state projection logic in the TicketState class, where the relevant fields’ values are mutated according to the events’ data:
50 public class TicketState
51 {
52 public TicketId Id { get; private set; }
53 public int Version { get; private set; }
54 public bool IsEscalated { get; private set; }
55 ...
56 public void Apply(TicketInitialized @event)
57 {
58 Id = @event.Id;
59 Version = 0;
60 IsEscalated = false;
61 ....
62 }
63
64 public void Apply(TicketEscalated @event)
65 {
66 IsEscalated = true;
67 Version += 1;
68 }
69
70 ...
71 }
Now let’s look at some of the advantages of leveraging event sourcing when imple‐ menting complex business logic.
![]() |