Event Sourcing
Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowchart; it’ll be obvious.
—Fred Brooks1
Let’s use Fred Brooks’s reasoning to define the event sourcing pattern and understand how it differs from traditional modeling and persisting of data. Examine Table 7-1 and analyze what you can learn from this data about the system it belongs to.
![]() |
1 Brooks, F. P. Jr. (1974). The Mythical Man-Month: Essays on Software Engineering. Reading, MA: Addison- Wesley.
lead- id | first-name | last- name | status | phone- number | followup-on | created-on | updated-on |
1 | Sean | Callahan | CONVERTED | 555-1246 |
| 2019-01-31T 10:02:40.32Z | 2019-01-31T 10:02:40.32Z |
2 | Sarah | Estrada | CLOSED | 555-4395 |
| 2019-03-29T 22:01:41.44Z | 2019-03-29T 22:01:41.44Z |
3 | Stephanie | Brown | CLOSED | 555-1176 |
| 2019-04-15T 23:08:45.59Z | 2019-04-15T 23:08:45.59Z |
4 | Sami | Calhoun | CLOSED | 555-1850 |
| 2019-04-25T 05:42:17.07Z | 2019-04-25T 05:42:17.07Z |
5 | William | Smith | CONVERTED | 555-3013 |
| 2019-05-14T 04:43:57.51Z | 2019-05-14T 04:43:57.51Z |
6 | Sabri | Chan | NEW_LEAD | 555-2900 |
| 2019-06-19T 15:01:49.68Z | 2019-06-19T 15:01:49.68Z |
7 | Samantha | Espinosa | NEW_LEAD | 555-8861 |
| 2019-07-17T 13:09:59.32Z | 2019-07-17T 13:09:59.32Z |
8 | Hani | Cronin | CLOSED | 555-3018 |
| 2019-10-09T 11:40:17.13Z | 2019-10-09T 11:40:17.13Z |
9 | Sian | Espinoza | FOLLOWUP_SET | 555-6461 | 2019-12-04T 01:49:08.05Z | 2019-12-04T 01:49:08.05Z | 2019-12-04T 01:49:08.05Z |
10 | Sophia | Escamilla | CLOSED | 555-4090 |
| 2019-12-06T 09:12:32.56Z | 2019-12-06T 09:12:32.56Z |
11 | William | White | FOLLOWUP_SET | 555-1187 | 2020-01-23T 00:33:13.88Z | 2020-01-23T 00:33:13.88Z | 2020-01-23T 00:33:13.88Z |
12 | Casey | Davis | CONVERTED | 555-8101 |
| 2020-05-20T 09:52:55.95Z | 2020-05-27T 12:38:44.12Z |
13 | Walter | Connor | NEW_LEAD | 555-4753 |
| 2020-04-20T 06:52:55.95Z | 2020-04-20T 06:52:55.95Z |
14 | Sophie | Garcia | CONVERTED | 555-1284 |
| 2020-05-06T 18:47:04.70Z | 2020-05-06T 18:47:04.70Z |
15 | Sally | Evans | PAYMENT_FAILED | 555-3230 |
| 2020-06-04T 14:51:06.15Z | 2020-06-04T 14:51:06.15Z |
16 | Scott | Chatman | NEW_LEAD | 555-6953 |
| 2020-06-09T 09:07:05.23Z | 2020-06-09T 09:07:05.23Z |
17 | Stephen | Pinkman | CONVERTED | 555-2326 |
| 2020-07-20T 00:56:59.94Z | 2020-07-20T 00:56:59.94Z |
18 | Sara | Elliott | PENDING_PAYMENT | 555-2620 |
| 2020-08-12T 17:39:43.25Z | 2020-08-12T 17:39:43.25Z |
19 | Sadie | Edwards | FOLLOWUP_SET | 555-8163 | 2020-10-22T 12:40:03.98Z | 2020-10-22T 12:40:03.98Z | 2020-10-22T 12:40:03.98Z |
20 | William | Smith | PENDING_PAYMENT | 555-9273 |
| 2020-11-13T 08:14:07.17Z | 2020-11-13T 08:14:07.17Z |
It’s evident that the table is used to manage potential customers, or leads, in a tele‐ marketing system. For each lead, you can see their ID, their first and last names, when the record was created and updated, their phone number, and the lead’s current status.
By examining the various statuses, we can also assume the processing cycle each potential customer goes through:
• The sales flow starts with the potential customer in the NEW_LEAD status.
• A sales call can end with the person not being interested in the offer (the lead is CLOSED), scheduling a follow-up call (FOLLOWUP_SET), or accepting the offer (PENDING_PAYMENT).
• If the payment is successful, the lead is CONVERTED into a customer. Conversely, the payment can fail—PAYMENT_FAILED.
That’s quite a lot of information that we can gather just by analyzing a table’s schema and the data stored in it. We can even assume what ubiquitous language was used when modeling the data. But what information is missing from that table?
The table’s data documents the leads’ current states, but it misses the story of how each lead got to their current state. We can’t analyze what was happening during the lifecycles of leads. We don’t know how many calls were made before a lead became CONVERTED. Was a purchase made right away, or was there a lengthy sales journey? Based on the historical data, is it worth trying to contact a person after multiple follow-ups, or is it more efficient to close the lead and move to a more promising prospect? None of that information is there. All we know are the leads’ current states.
These questions reflect business concerns essential for optimizing the sales process. From a business standpoint, it’s crucial to analyze the data and optimize the process based on the experience. One of the ways to fill in the missing information is to use event sourcing.
The event sourcing pattern introduces the dimension of time into the data model. Instead of the schema reflecting the aggregates’ current state, an event sourcing– based system persists events documenting every change in an aggregate’s lifecycle.
Consider the CONVERTED customer on line 12 in Table 7-1. The following listing dem‐ onstrates how the person’s data would be represented in an event-sourced system:
{
"lead-id": 12,
"event-id": 0,
"event-type": "lead-initialized",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-2951",
"timestamp": "2020-05-20T09:52:55.95Z"
},
{
"lead-id": 12,
"event-id": 1,
"event-type": "contacted",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 2,
"event-type": "followup-set",
"followup-on": "2020-05-27T12:00:00.00Z",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 3,
"event-type": "contact-details-updated",
"first-name": "Casey",
"last-name": "Davis",
"phone-number": "555-8101",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 4,
"event-type": "contacted",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 5,
"event-type": "order-submitted",
"payment-deadline": "2020-05-30T12:02:12.51Z",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 6,
"event-type": "payment-confirmed",
"status": "converted",
"timestamp": "2020-05-27T12:38:44.12Z"
}
The events in the listing tell the customer’s story. The lead was created in the system (event 0) and was contacted by a sales agent about two hours later (event 1). During the call, it was agreed that the sales agent would call back a week later (event 2), but to a different phone number (event 3). The sales agent also fixed a typo in the last name (event 3). The lead was contacted on the agreed date and time (event 4) and submit‐ ted an order (event 5). The order was to be paid in three days (event 5), but the
payment was received about half an hour later (event 6), and the lead was converted into a new customer.
As we saw earlier, the customer’s state can easily be projected out from these domain events. All we have to do is apply simple transformation logic sequentially to each event:
public class LeadSearchModelProjection
{
public long LeadId { get; private set; }
public HashSet<string> FirstNames { get; private set; }
public HashSet<string> LastNames { get; private set; }
public HashSet<PhoneNumber> PhoneNumbers { get; private set; }
public int Version { get; private set; }
public void Apply(LeadInitialized @event)
{
LeadId = @event.LeadId;
FirstNames = new HashSet<string>(); LastNames = new HashSet<string>(); PhoneNumbers = new HashSet<PhoneNumber>(); FirstNames.Add(@event.FirstName); LastNames.Add(@event.LastName); PhoneNumbers.Add(@event.PhoneNumber); Version = 0;
}
public void Apply(ContactDetailsChanged @event)
{
FirstNames.Add(@event.FirstName); LastNames.Add(@event.LastName); PhoneNumbers.Add(@event.PhoneNumber); Version += 1;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollowupSet @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Version += 1;
}
}
Iterating an aggregate’s events and feeding them sequentially into the appropriate overrides of the Apply method will produce precisely the state representation mod‐ eled in the table in Table 7-1.
Pay attention to the Version field that is incremented after applying each event. Its value represents the total number of modifications made to the business entity. More‐ over, suppose we apply a subset of events. In that case, we can “travel through time”: we can project the entity’s state at any point of its lifecycle by applying only the rele‐ vant events. For example, if we need the entity’s state in version 5, we can apply only the first five events.
Finally, we are not limited to projecting only a single state representation of the events! Consider the following scenarios.