Event Sourcing: Architecture, Patterns, and When to Use It
Most applications store current state. An order is "shipped," a balance is "$42.50," a user's email is "alice@example.com." You overwrite the old value with the new one, and the history is gone. Event sourcing takes a fundamentally different approach: instead of storing current state, you store the sequence of events that produced it.
Event Sourcing vs. CRUD#
In a traditional CRUD system, the database holds the latest snapshot of every entity. In an event-sourced system, the database holds an append-only log of domain events.
CRUD: UPDATE accounts SET balance = 42.50 WHERE id = 1;
Event Sourcing:
1. AccountOpened { id: 1, owner: "Alice" }
2. MoneyDeposited { id: 1, amount: 100.00 }
3. MoneyWithdrawn { id: 1, amount: 57.50 }
To know Alice's balance, you replay events 1 through 3. The current state is a left fold over the event stream.
Why This Matters#
- Full audit trail — Every state change is recorded. You never lose information.
- Temporal queries — "What was Alice's balance at 3 p.m. on Tuesday?" Just replay up to that timestamp.
- Debugging — Reproduce any bug by replaying the exact sequence of events that caused it.
- Decoupling — Other services subscribe to events and build their own read models without coupling to your schema.
The Event Store#
The event store is the heart of an event-sourced system. It is an append-only, ordered log of events, partitioned by aggregate (or stream).
Stream: account-1
┌───┬──────────────────┬─────────────────────────────┬──────────┐
│ # │ Event Type │ Data │ Timestamp│
├───┼──────────────────┼─────────────────────────────┼──────────┤
│ 0 │ AccountOpened │ { owner: "Alice" } │ 09:00:01 │
│ 1 │ MoneyDeposited │ { amount: 100.00 } │ 09:05:12 │
│ 2 │ MoneyWithdrawn │ { amount: 57.50 } │ 09:07:44 │
│ 3 │ MoneyDeposited │ { amount: 20.00 } │ 10:15:03 │
└───┴──────────────────┴─────────────────────────────┴──────────┘
Key properties of an event store:
- Append-only — Events are immutable. You never update or delete them.
- Ordered — Each event has a sequence number within its stream.
- Optimistic concurrency — Writes specify the expected version. If another write happened first, you get a conflict.
- Subscriptions — Consumers can subscribe to streams and receive events in real time.
Projections and Read Models#
Replaying thousands of events for every read query is impractical. Instead, you build projections — pre-computed read models derived from the event stream.
Events ──▶ Projection Handler ──▶ Read Model (Postgres, Redis, Elasticsearch)
A projection listens to events and updates a denormalized view optimized for queries:
function handleEvent(state: AccountView, event: DomainEvent): AccountView {
switch (event.type) {
case "AccountOpened":
return { id: event.data.id, owner: event.data.owner, balance: 0 };
case "MoneyDeposited":
return { ...state, balance: state.balance + event.data.amount };
case "MoneyWithdrawn":
return { ...state, balance: state.balance - event.data.amount };
}
}
You can create multiple projections from the same event stream: one for the account dashboard, one for the fraud detection engine, one for the analytics warehouse. Each is independently deployable and rebuildable.
Snapshotting#
For aggregates with long event histories, replaying from event zero on every command becomes slow. Snapshotting solves this by periodically saving the current state alongside the event position.
Events: 0 ──── 1 ──── 2 ──── ... ──── 999 ──── 1000 ──── 1001
▲
Snapshot at #999
{ balance: 42.50 }
To rebuild, load the snapshot at event 999, then replay only events 1000 and 1001. Common strategies:
- Every N events — Snapshot every 100 or 1000 events.
- Time-based — Snapshot every hour.
- On demand — Snapshot when replay time exceeds a threshold.
Rebuilding State#
One of event sourcing's superpowers is the ability to rebuild any read model from scratch. Deployed a bug in your projection logic? Fix the code, delete the read model, replay all events, and the data is correct again.
This also enables schema evolution of read models. Need a new column? Add it to the projection handler, replay, and the column is populated for all historical data.
CQRS with Event Sourcing#
Command Query Responsibility Segregation (CQRS) separates the write model from the read model. Event sourcing pairs naturally with CQRS:
┌──────────────┐
Command ────────▶│ Aggregate │──── Events ────▶ Event Store
│ (Write) │ │
└──────────────┘ │
▼
┌─────────────┐
Query ─────────────────────────────────────────▶│ Projection │
│ (Read Model)│
└─────────────┘
- Commands validate business rules against the aggregate and produce events.
- Events are persisted to the event store.
- Projections consume events and update read-optimized stores.
- Queries hit the read models directly — fast and denormalized.
The write side and read side can scale independently and use different storage technologies.
Tools and Frameworks#
EventStoreDB#
A purpose-built database for event sourcing. Native support for streams, subscriptions, projections, and optimistic concurrency. gRPC and HTTP APIs. The most mature dedicated event store.
Axon Framework (Java/Kotlin)#
A framework that provides building blocks for event sourcing and CQRS: aggregates, command handlers, event handlers, sagas, and snapshotting. Axon Server acts as the event store and message router.
Marten (.NET)#
A document database and event store library built on top of PostgreSQL. Lets .NET teams adopt event sourcing without introducing a new database — Postgres handles both events and projections.
Other Notable Tools#
- Eventuous — Lightweight .NET library for event sourcing.
- Prooph — PHP event sourcing and CQRS components.
- commanded — Elixir CQRS/ES framework.
- Kafka — Not an event store (no per-aggregate streams or optimistic concurrency), but widely used as the transport layer between event stores and projections.
Eventual Consistency#
In a CQRS + event sourcing system, projections are eventually consistent with the write model. After a command produces an event, there is a delay before the projection processes it.
Strategies for handling this:
- Causal consistency — After a write, redirect the user to a page that reads from the write model (or waits for the projection to catch up to the known event version).
- Polling / SSE — The client polls or subscribes until the read model reflects the expected version.
- Accept it — For many use cases (dashboards, reports, search), a few hundred milliseconds of staleness is invisible to users.
Versioning Events#
Events are immutable, but your domain evolves. How do you handle schema changes?
- Weak schema — Store events as JSON and handle missing fields with defaults in the consumer.
- Upcasting — Transform old event shapes into the current shape at read time. Event version 1 is upcast to version 2 before the projection sees it.
- New event types — Introduce a new event type (e.g.,
MoneyWithdrawnV2) and let projections handle both old and new. - Copy-and-transform — Migrate the entire event store to a new stream with transformed events. Heavy but sometimes necessary.
The key rule: never modify existing events in place. They are your source of truth.
When to Use Event Sourcing#
Event sourcing shines when:
- Audit is critical — Finance, healthcare, legal, compliance-heavy domains.
- Temporal queries matter — "Show me the state at any point in time."
- Multiple read models — Different consumers need different views of the same data.
- Complex domain logic — Domain-driven design aggregates with rich business rules benefit from explicit event modeling.
- Event-driven architecture — If your system is already event-driven, event sourcing formalizes the pattern.
When It Is Overkill#
Event sourcing adds complexity. Skip it when:
- Simple CRUD — A basic settings page or user profile does not need an event log.
- No audit requirement — If you do not need history, do not pay the cost of storing it.
- Small team, early stage — The learning curve and infrastructure overhead slow down teams that need to iterate fast.
- Reporting on current state only — If you only ever query "what is it now," a traditional database is simpler.
- Tight consistency requirements — If every read must reflect the absolute latest write, eventual consistency adds friction.
Start with CRUD. Adopt event sourcing for the bounded contexts where its benefits outweigh its costs. Not every service in a system needs to be event-sourced.
Designing event-driven architectures? Codelit helps teams model, visualize, and document systems — from event flows to aggregate boundaries — so your architecture decisions are clear to everyone on the team.
Article #178 on codelit.io
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
AI Agent Tool Use Architecture: Function Calling, ReAct Loops & Structured Outputs
6 min read
AI searchAI-Powered Search Architecture: Semantic Search, Hybrid Search, and RAG
8 min read
AI safetyAI Safety Guardrails Architecture: Input Validation, Output Filtering, and Human-in-the-Loop
8 min read
Try these templates
Netflix Video Streaming Architecture
Global video streaming platform with adaptive bitrate, CDN distribution, and recommendation engine.
10 componentsSearch Engine Architecture
Web-scale search with crawling, indexing, ranking, and sub-second query serving.
8 componentsGoogle Search Engine Architecture
Web-scale search with crawling, indexing, PageRank, query processing, ads, and knowledge graph.
10 components
Comments