Event Sourcing & CQRS
Implement event sourcing: append-only event store, aggregate reconstruction, CQRS with separate read/write models, and projection updates.
Event sourcing stores state as a sequence of events rather than current state. CQRS separates read and write models. Together they enable temporal queries, audit logs, event replay, and scalable read models - at the cost of complexity. Use only where the benefits justify the overhead.
Event sourcing is a powerful but complex pattern. Only adopt it when you need an audit log, temporal queries, or event replay as a core business requirement - not as an architectural default. Start with a simple CRUD system and refactor to event sourcing if/when you hit specific limitations.
Workflow
- Design the event storeThe event store is the source of truth - events are immutable and never deleted.
- Model aggregates and eventsAggregates encapsulate business logic; events describe what happened (not what to do).
- Implement CQRS with projectionsSeparate read and write models: write model (events) is normalized; read model (projections) is denormalized for query efficiency.
- Handle event versioning and migrationEvents are immutable but their schemas evolve - event versioning prevents breaking old events.
- Rebuild projections via event replayEvent replay lets you create new views of historical data or fix corrupted projections.
Output Format
PostgreSQL event store with optimistic concurrency, aggregate with `apply`/`evolve`, CQRS projection table per view, event upcasting for version migration, snapshot for performance, full replay capability.
Examples
Append event: `async function appendEvent(streamId: string, expectedVersion: number, event: Event) { try { await db.query("INSERT INTO events (stream_id, stream_version, event_type, payload) VALUES ($1, $2, $3, $4)", [streamId, expectedVersion + 1, event.type, event.payload]) } catch (e) { if (isUniqueViolation(e)) throw new ConcurrencyError("Stream version conflict") throw e } }`Load aggregate: `async function loadOrder(orderId: string): Promise<Order> { const events = await db.query("SELECT * FROM events WHERE stream_id = $1 ORDER BY stream_version", [orderId]); return Order.reconstitute(events.rows.map(r => ({ type: r.event_type, ...r.payload }))) }`Projection with position: `SELECT e.* FROM events e WHERE e.id > (SELECT last_position FROM projection_positions WHERE name = "order-summary") ORDER BY e.id LIMIT 100`Edge Cases & Gotchas
- Eventually consistent read models: after writing an event, the projection is updated asynchronously - clients may read stale data. For UIs that need immediate read-your-writes consistency, return the new state in the write response
- Long-lived aggregates: an aggregate with 10,000 events is slow to reconstitute. Implement snapshots at N events: store current state, load snapshot + only events after it
- Saga/process managers: multi-step workflows spanning multiple aggregates require sagas - listen to events from one aggregate, trigger commands on another
When to Break These Rules
- For most CRUD apps - user profiles, blog posts, configuration - event sourcing adds complexity without benefit. If you don't need audit history, temporal queries, or event replay, use a standard relational model.
Reference Files
EventStoreDB: https://www.eventstore.com/eventstoredbEvent Sourcing pattern: https://martinfowler.com/eaaDev/EventSourcing.htmlCQRS: https://martinfowler.com/bliki/CQRS.htmlAxon Framework concepts: https://docs.axoniq.io/axon-framework-reference/4.10/