Event Sourcing for Application Developers

Stackademic

Learn how event sourcing stores state as a log of facts, when it beats CRUD tables, and patterns for projections, versioning, and replays.

Event sourcing stores application state as an append-only sequence of domain events instead of overwriting rows in place. Teams adopt it when audit trails, temporal queries, or reliable integration across services matter more than the simplicity of a single “current state” table.

The core idea

In a CRUD model, a bank account row holds a balance you update. In event sourcing, you store facts: AccountOpened, MoneyDeposited, MoneyWithdrawn. The balance is derived by replaying events (or reading a cached projection). The database of record is the event log; projections are disposable as long as you can rebuild them.

That shift changes how you think about validation, concurrency, and debugging—you debug timelines, not only snapshots.

When event sourcing is worth the complexity

Good fits:

  • Regulated domains needing immutable history (finance, healthcare workflows with audit)
  • Collaborative domains where “who changed what when” is product-critical
  • Integrations where downstream systems consume facts rather than polled state
  • Temporal questions such as “what did we know at close of business yesterday?”

Poor fits:

  • Simple CRUD admin panels with no audit requirements
  • Teams without operational maturity for replay tooling and schema migration
  • Read-heavy pages that cannot tolerate projection lag without caching investment

If you only need an audit table beside CRUD, consider outbox + change data capture before full event sourcing.

Building blocks in production

Event store

An append-only log per aggregate (order, account, device). Stores event type, payload version, metadata (user, correlation id), and monotonic sequence numbers. PostgreSQL with careful indexing, Kafka topics keyed by aggregate id, or specialized event stores each work; choose based on ops skills.

Aggregates and commands

Commands express intent (PlaceOrder). The aggregate validates business rules against its history and emits events (OrderPlaced). Keep aggregates small to reduce contention—split hot fields into separate streams if needed.

Projections (read models)

Consumers build query-friendly views: SQL tables, search indexes, caches. Projections may lag briefly; UX should tolerate eventual consistency or use read-your-writes patterns where required.

Snapshots

Replaying thousands of events per request is slow. Periodic snapshots store aggregate state at a sequence number so replay starts mid-stream.

Versioning events without breaking replays

Schemas evolve. Practices that save teams:

  • Additive fields with defaults in new event versions
  • Upcasters that transform old events to new shapes during replay
  • Explicit eventVersion in payloads
  • Contract tests that replay golden fixtures in CI

Never edit events in place. Compensating events (PaymentRefunded) correct mistakes; destructive edits destroy trust.

Concurrency and idempotency

Use expected-version checks on append: “save only if current sequence is 41.” Conflicts mean another writer won; retry with refreshed history. Command handlers must be idempotent where transports retry—store processed command ids.

CQRS: optional but common

Command Query Responsibility Segregation separates write models (aggregates) from read models (projections). You can event-source writes without CQRS, but most teams pair them so APIs stay fast while the log stays honest.

Failure modes to rehearse

RiskMitigation
Projection bug corrupts readsRebuild projection from scratch
Poison event blocks consumerQuarantine + manual fix forward
Hot aggregate contentionSplit model or shard streams
Unbounded log growthArchival tiers + snapshot policy

Run game days that rebuild a staging projection from production exports (sanitized).

Minimal example (conceptual)

Command: Deposit($50)
Events:  MoneyDeposited { amount: 50, currency: "USD" }
Projection SQL: UPDATE balances SET amount = amount + 50 WHERE id = ...

The projection update is not the source of truth; the event is. If the projection drifts, replay fixes it.

Testing strategy

  • Given a list of events, assert aggregate state and emitted events for each command
  • Property-based tests on replay: random event order within aggregate should not matter
  • Golden replays for each schema version bump

FAQ

Is this the same as event-driven architecture?
Related but not identical. Event-driven systems pass messages between services. Event sourcing is a persistence pattern for state inside a service (often emitting integration events too).

Do I need Kafka?
No. Many teams start with PostgreSQL tables storing events and background workers building projections.

How do deletes work?
Model them as events (UserDeactivated) or tombstone events; physical erasure may still be required for privacy regulations—plan GDPR deletes with legal, not only engineering.

What about reports?
Use projections tailored to reporting, or stream events to a warehouse with batch transforms.

Practical adoption path

Start with one bounded context (billing adjustments, inventory reservations). Keep CRUD elsewhere. Instrument lag, replay time, and consumer errors. Expand only when the team enjoys debugging timelines more than they hate migration scripts.

Tooling that pays off early

Invest in a replay CLI that rebuilds projections for a date range, a web UI or SQL view to inspect an aggregate’s timeline, and metrics on append latency versus consumer lag. Without visibility, event-sourced systems feel “mysterious” to new hires. Document how to fix a bad event: compensating command, manual patch procedure, and who approves it.

Comparing event sourcing to audit tables

Audit tables capture who changed rows; event sourcing makes changes the primary artifact. Audit logs are easier to bolt onto CRUD but weak at rebuilding past state or branching logic. If your compliance ask is only “show me edits,” audit may suffice. If product needs time travel or rich integrations, events earn their keep.

Security and privacy considerations

Event logs can contain PII long after users request deletion. Design retention policies, encryption at rest, and field-level redaction in projections. Role-based access to replay tools is as important as production database permissions—an exported event stream is a backup of your most sensitive history.

Performance tuning notes

Partition streams by aggregate id to keep hot keys from blocking unrelated traffic. Compress large payloads in object storage references rather than inline blobs. For read APIs, serve projections with read replicas; never replay an entire aggregate on every HTTP GET unless aggregates are tiny. Cache snapshot metadata in Redis when cold starts hurt serverless workers.

Sample event envelope

Keep envelopes boring and consistent:

{
  "eventId": "uuid",
  "aggregateId": "order-9812",
  "sequence": 42,
  "type": "OrderShipped",
  "version": 1,
  "occurredAt": "2026-10-20T15:04:00Z",
  "payload": { "carrier": "ups", "tracking": "1Z..." }
}

Metadata (correlationId, causationId, actor) belongs beside payload, not buried inside business fields. Consumers rely on stable typing for routing and schema registry entries.

When to stop event sourcing

If teams spend more time fighting replay tooling than shipping features, narrow scope or revert the hottest aggregate to CRUD with an outbox for integrations. Event sourcing is a scalpel, not a default database style.

Read models for operators

Support staff rarely want raw JSON events. Build operator-facing projections with friendly labels, hyperlinks to related aggregates, and safe actions (retry shipment, issue credit) implemented as commands—not ad hoc SQL updates. When operators trust the timeline UI, engineering gets faster feedback on bad events because mistakes surface in minutes instead of quarterly audits.

Treat projections as products: version their schemas, announce breaking changes to internal consumers, and keep backward-compatible readers during migrations just like you would for a public API.