open atlas
↑ Back to track
Architecture Patterns ARCH · 08 · 01

Events as the source of truth

Event sourcing stores state as an append-only log of immutable facts. Current state is not stored directly — it is derived by folding (reducing) the event log. Events record what happened and why, not just the resulting after-image.

ARCH Senior ◷ 26 min
Level
FoundationsJuniorMiddleSenior

The billing team got a call from the CFO at 9 PM: “Tell me exactly what state order #48291 was in on March 3rd at 2 PM, and why it changed from ‘pending’ to ‘approved’ that afternoon.” The team opened the database. The orders table had one row for order #48291 with the current status: “invoiced.” The history was gone. The table stored only the last state. There was no record of when it was approved, who triggered it, what the line items were at approval time, or why the status changed. The audit trail was reconstructed from application logs — unreliable, incomplete, deployed-out-of-sync. It took six hours to produce a partial answer. The CFO was not satisfied. This team had built a perfectly normal CRUD system. Every UPDATE orders SET status = 'approved' destroyed the previous state. The why — the business event — was nowhere in the schema. What they needed was not a better query or a better index. They needed a different model of what data to store in the first place.

The CRUD model destroys history

A conventional CRUD system stores current state. When an order is approved, the application issues an UPDATE orders SET status = 'approved' WHERE id = 48291. The previous state — pending — is gone. The database has no record of the transition: not when it happened, not what triggered it, not what the payload was at the time of the decision.

This is not a bug in the application. It is a consequence of the data model. Relational tables are designed to hold the latest snapshot of an entity. That model is efficient, familiar, and appropriate for a large class of problems. But it has a structural consequence: the history of how an entity arrived at its current state is discarded by default.

The team reconstructing the CFO’s audit trail was doing manually what the data model should have preserved automatically. They were reverse-engineering history from logs, commit timestamps, and memory.

The event sourcing model: append only, never overwrite

Event sourcing inverts this. Instead of storing the current state of an order, you store every business event that has ever happened to that order — in an append-only log. The row in the events table for order #48291 looks like:

seq  event_type           payload                      occurred_at
1    OrderPlaced          {customer_id, line_items}    2024-03-01 09:12
2    OrderReviewStarted   {reviewer_id}                2024-03-02 14:33
3    OrderApproved        {approved_by, reason}        2024-03-03 14:07
4    InvoiceGenerated     {invoice_id, amount}         2024-03-04 11:55

These rows are never updated, never deleted. An event that happened is a fact in the past tense. OrderPlaced is not a description of the current state — it is a record that a placement happened. The event store is the structural expression of that immutability.

Current state is a left-fold over the event log

If the event log is the source of truth, how do you get the current state of an order? You fold (reduce) the events from left to right, applying each event to an accumulator that starts as empty:

currentState = events.reduce(applyEvent, initialState)

applyEvent is a pure function: given the current accumulated state and an event, it returns the next state. It contains no I/O, no side effects. For each event type, it knows how to advance the state:

applyEvent(state, OrderPlaced)         → state with status: "pending", line_items: [...]
applyEvent(state, OrderReviewStarted)  → state with status: "under_review", reviewer_id: ...
applyEvent(state, OrderApproved)       → state with status: "approved", approved_by: ...
applyEvent(state, InvoiceGenerated)    → state with status: "invoiced", invoice_id: ...

The result of the fold is the current state of the aggregate. It is derived, not stored. When the application needs to process a command against order #48291, it loads the event log for that order and folds it to reconstruct the current state. The state is never written to a normalized column.

lesson.inset.note

This is the structural relationship between event sourcing and CQRS (unit 07). Event sourcing concerns the write side — how the aggregate’s state is stored. CQRS is a separate pattern that separates the read model from the write model. They compose well but are independent: you can use event sourcing without CQRS, and CQRS without event sourcing. The CQRS projections lesson (unit 07) touched on replay from an event log; that is event sourcing doing the work.

Events capture intent, not just the after-image

A critical property of well-named events: they record what happened at the business level, not just the resulting data change. OrderApproved is not the same as “status column changed from under_review to approved.” The name carries intent: a business decision was made to approve the order. The payload carries context: who approved it, the reason code, the approval threshold in effect at the time.

This distinction matters when the domain logic is complex. Consider a line item quantity change:

  • CRUD: UPDATE order_items SET quantity = 5 WHERE id = 77 — after-image only
  • Event sourcing: LineItemQuantityAdjusted { item_id: 77, previous_qty: 3, new_qty: 5, adjusted_by: "ops-team", reason: "supplier_substitution" } — intent + context

Six months later, when a dispute arises about why the quantity changed, the event sourced system can answer exactly. The CRUD system has only the number 5.

Quiz

The engineering team is debating whether to use event sourcing for the B2B order platform's order aggregate. A developer argues: 'We can get audit history by enabling database-level change tracking (CDC) or keeping a separate audit log table. We don't need to change our entire data model.' What is the structural difference between CDC/audit-log approaches and genuine event sourcing?

The audit-trail and temporal-query superpower

The structural consequence of storing the event log as source of truth is that the CFO’s question — “what was the state of order #48291 on March 3rd at 2 PM?” — becomes trivial:

events.filter(e => e.occurred_at <= "2024-03-03T14:00").reduce(applyEvent, initialState)

Filter the event log to only include events before the target timestamp, then fold. The result is the exact state at that moment. No reconstruction from logs, no partial answers. This is a temporal query: deriving the state of an entity at any point in its past.

Temporal queries are not a bolt-on feature in event sourced systems. They are a structural consequence of the data model. The event log preserves every state transition; folding a prefix of it gives any past state. This makes event sourcing a natural fit for domains where auditability and temporal queries are first-class requirements: financial systems, compliance-regulated domains, logistics, healthcare records.

Quiz

A team is designing the order aggregate for an event sourced system. A developer proposes naming the event 'OrderStatusUpdated' with a payload of { new_status: 'approved' }. A senior engineer objects. What is the objection, and why does it matter?

What event sourcing is not: a replacement for CRUD everywhere

Event sourcing imposes structural costs. The event log for a heavily-mutated aggregate accumulates indefinitely. Loading an aggregate with thousands of events requires folding them all — performance degrades without mitigation (snapshots, covered in unit 08 lesson 2). Querying the current state of many aggregates simultaneously requires a read model (a projection) built from the event log, which means running and maintaining a projection pipeline. The write side and the read side are now separate systems.

These are not theoretical costs. Teams that apply event sourcing to every entity in a system — including entities with no audit or temporal-query requirements — accumulate these costs without the benefit. Event sourcing is a fit for aggregates where:

  • The history of state transitions is a first-class business requirement (audit, compliance, dispute resolution)
  • Temporal queries (“as of” a past date) are needed
  • The intent behind each state change needs to be preserved as structured data
  • The domain logic is complex enough that the event log serves as a complete, inspectable record of business decisions

For a UserPreference entity that stores a UI toggle state, event sourcing is almost certainly overkill. Knowing that a user toggled dark mode seventeen times is not a business requirement.

Quiz

The billing team is loading an order aggregate to process a command. The order has 4,200 events in the log. The team's current implementation folds all 4,200 events on every command. What is the structural issue, and what is the standard mitigation in event sourcing?

Recall before you leave
  1. 01
    What does it mean for current state to be a 'left-fold over the event log'?
  2. 02
    Why do events in event sourcing use past-tense names like OrderApproved rather than imperative names like ApproveOrder?
  3. 03
    What is the key structural difference between event sourcing and adding an audit-log table to a CRUD system?
Recap

The CFO’s 9 PM call exposed the structural cost of the CRUD model: every update destroyed the previous state. The history of how the order arrived at its current status was gone. Reconstructing it from logs took six hours and produced a partial answer.

Event sourcing solves this structurally, not as an afterthought. The event log — an append-only sequence of immutable, past-tense business facts — is the source of truth. Current state is derived by folding the log, never stored directly. The fold is deterministic and reproducible. Temporal queries are free: filter the log to any past timestamp and fold the prefix.

Well-named events carry intent. OrderApproved with an approver and a reason code is not the same as a status column change. It is the difference between a data mutation and a business fact. That distinction is what makes the event log a complete, auditable record of business decisions — not just a history of row writes.

The structural cost is real: fold performance degrades with long event logs (mitigated by snapshots); read queries need a separate projection layer (covered next in lesson 2); schema evolution introduces versioning challenges (covered in lesson 3). Event sourcing is not the right model for every aggregate. It is the right model when the history of business decisions is a first-class requirement.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.