MESSAGING & EVENT-DRIVEN ARCHITECTURE / SYSTEM CONCEPT BRIEF

Event sourcing

Event sourcing stores every change to an entity as an immutable event in an append-only log, instead of storing only the current state.

AdvancedPhase 06 / Topic 10 of 18RequirementsTrade-offsFailure modes
01

Overview

Event sourcing stores every change to an entity as an immutable event in an append-only log, instead of storing only the current state. The current state is derived by replaying events: an account's balance is the sum of its Deposited and Withdrawn events. The event log becomes the source of truth.

This gives a complete audit trail, the ability to rebuild state at any point in time, and the freedom to create new read models by replaying history. The costs are more complex querying (usually requiring CQRS projections), event schema evolution, and snapshotting to avoid replaying very long histories.

A bank statement

Your bank does not just store 'balance = $1,240'. It stores every deposit and withdrawal. The balance is computed from those entries, and you can always see how you got there.

02

When to use it

  • Audit requirements: finance, healthcare, compliance.
  • Domains where history matters (ledgers, order lifecycles, version control).
  • Rebuilding or adding new views from past data.
  • Debugging by replaying exactly what happened.
03

Where it shows up in interviews

Ledgers

Recognize it when: every money movement must be auditable.

  • Design a digital wallet
  • Design a payments ledger
Temporal queries

Recognize it when: what was the state at a past time?

  • Design an insurance policy system
  • Design an order tracking timeline
04

Where it is used in real software

Git

Commits are an event log of changes; any version can be reconstructed.

Banking core systems

Ledgers record immutable entries; balances are derived and reconciled.

EventStoreDB and Kafka

Purpose-built event stores or Kafka topics with compaction hold event streams.

05

Key terms

Event
An immutable fact in the past tense (FundsDeposited).
Event stream
Ordered events for one aggregate.
Rehydration
Rebuilding state by replaying events.
Snapshot
Saved state at a version to shorten replay.
Upcasting
Converting old event versions to the new schema on read.
06

How it works, step by step

  1. 1
    Load the aggregate

    Read the snapshot plus later events and apply them.

  2. 2
    Handle a command

    Validate against current state.

  3. 3
    Emit new events

    Append with an expected version (optimistic concurrency).

  4. 4
    Publish events

    Projectors update read models.

  5. 5
    Snapshot periodically

    For example every 100 events.

07

Rebuilding an account from events

Stream for account A-1

Step 1 / 5
VersionEventBalance after
1AccountOpened$0
2FundsDeposited $500$500
3FundsWithdrawn $120$380
4FundsDeposited $60$440
5WithdrawalRejected $900 (insufficient)$440

NOWVersion: 1 | Event: AccountOpened | Balance after: $0

Current state is a fold over events. The rejected withdrawal is also recorded, which a state-only model would have forgotten.

08

Implementation

type AccountEvent =  | { type: "AccountOpened"; accountId: string }  | { type: "FundsDeposited"; amountCents: number }  | { type: "FundsWithdrawn"; amountCents: number }; type Account = { balanceCents: number; version: number }; const apply = (state: Account, event: AccountEvent): Account => {  switch (event.type) {    case "AccountOpened": return { balanceCents: 0, version: state.version + 1 };    case "FundsDeposited": return { balanceCents: state.balanceCents + event.amountCents, version: state.version + 1 };    case "FundsWithdrawn": return { balanceCents: state.balanceCents - event.amountCents, version: state.version + 1 };  }}; export async function withdraw(accountId: string, amountCents: number) {  const events = await store.readStream(accountId);  const account = events.reduce(apply, { balanceCents: 0, version: 0 });  if (account.balanceCents < amountCents) throw new Error("Insufficient funds");  // Optimistic concurrency: fails if another writer appended since we read  await store.append(accountId, [{ type: "FundsWithdrawn", amountCents }], { expectedVersion: account.version });}
09

Complexity and performance

RehydrateO(events since snapshot)

Keep it bounded.

AppendO(1)

With version check.

10

Trade-offs

Auditability vs query complexity

Full history and replay come at the cost of needing projections for queries.

Immutability vs privacy

Deleting personal data from immutable logs requires crypto-shredding (encrypt per user, delete the key).

11

Variants and related techniques

Snapshots

Periodic saved state for faster loads.

Event-carried state transfer

Events carry enough data for consumers to avoid calling back.

12

Common mistakes

  • Changing the meaning of old events.

    Fix: Events are immutable; add new event types or versions and upcast.

  • Using event sourcing for simple CRUD.

    Fix: The complexity only pays off when history matters.

  • Events that describe technical changes (RowUpdated).

    Fix: Model business facts (OrderShipped).

13

Interview questions

What are the benefits of event sourcing?

A complete audit log, the ability to reconstruct state at any time, easy creation of new read models by replay, and a natural fit with event-driven integration.

How do you handle concurrency in an event-sourced aggregate?

Append with an expected version. If another writer appended first, the append fails, and the command reloads state and retries or rejects.

14

Practice problems

ProblemDifficultyWhat it trains
Implement an event-sourced bank accountMediumFold and versioning.
Design an event-sourced order system with projectionsHardCQRS + snapshots.