A checkout function charges the card, then crashes before creating the shipment. It restarts, the database row still reads "processing," and nothing on disk records which steps ran. The fix teams reach for is a new persistence architecture. The more useful question is which layer of the stack should own the log, and whether you need to build that layer yourself.

This guide covers what event sourcing is, how it differs from event-driven architecture and audit logging, and the three components any implementation needs.

**Key takeaways:**

- Event sourcing records every state change as an immutable event in an append-only log, and current state is always rebuilt by replaying that log.

- Event sourcing is a persistence pattern, whereas event-driven architecture is a communication pattern and audit logging is a compliance record. The three are routinely confused.

- Read models are disposable, so fixing a projection bug means correcting the code and rebuilding rather than migrating production rows.

- Schema evolution is the most underestimated cost, on the grounds that every historical event has to stay processable by current code permanently.

- On serverless, the durable store holds the log of record and Vercel Queues carry events to projection workers, whereas Vercel Workflows apply the same pattern to execution state.

## [Copy link to heading](#what-is-event-sourcing)What is event sourcing?

Event sourcing is a persistence model that records every state change as an immutable event in an append-only log. Current state is derived by replaying that log, either from the beginning or from a snapshot. The log is the database. Nothing is overwritten in place, and a correction is a new event rather than an edit to an old one.

Version control already works this way. The commit history is the source of truth and the working copy is a derived view you can regenerate at any point in history. The events themselves capture business intent rather than field-level changes. Recording `OrderPlaced` names what the business did, whereas a diff of the orders table names only the columns that moved.

### [Copy link to heading](#how-event-sourcing-differs-from-event-driven-architecture-and-audit-logging)How event sourcing differs from event-driven architecture and audit logging

Three patterns get conflated because all three append entries to something. Event sourcing is a persistence pattern; [event-driven architecture](https://vercel.com/i/event-driven-architecture) is a communication pattern; and audit logging is a compliance record. Only the first treats its log as the state of the system. The distinction shows up across the dimensions that drive design decisions:

| Dimension | Event sourcing | Event-driven architecture | Audit logging |
| --- | --- | --- | --- |
| Primary role | System of record | Transport between services | Compliance and forensics |
| Where current state lives | Derived from the log | In each service's own database | In the application's tables, untouched |
| What an entry captures | Domain intent at write time | A message for subscribed consumers | A row-level before and after |
| Can you rebuild state from it | Yes, by design | No | No |
| Main dependency | None required | A bus or queue | A database trigger or a log drain |

The rebuild row is the practical tell. An audit log describes what changed and an event log \*is\* what changed; deleting an audit row loses a record, whereas deleting an event loses the state itself.

### [Copy link to heading](#benefits-of-event-sourcing-for-production-systems)Benefits of event sourcing for production systems

Event sourcing never destroys a state transition, so any past moment stays reachable. That single property produces 4 distinct capabilities:

- **Complete audit history:** Every state transition becomes a first-class record rather than a side effect of logging, which is why financial systems adopt the pattern first. Stripe's Ledger tracks all money movement as an immutable log, ingesting [5 billion events](https://stripe.dev/blog/ledger-stripe-system-for-tracking-and-validating-money-movement) a day.

- **Tamper-evidence by construction:** An append-only store makes alteration detectable rather than merely discouraged by policy. Uber's LedgerStore signs its entries cryptographically to keep them verifiably immutable, at a scale of [trillions of indexes](https://www.uber.com/us/en/blog/how-ledgerstore-supports-trillions-of-indexes/).

- **State at any past moment:** Stopping a replay early produces the exact state at that point in history. A question like what an account balance was on a given date is answered from the log itself, with no snapshot table maintained for the purpose.

- **Retroactive fixes by replay:** A read model is derived rather than authoritative, so a bug in projection logic is repaired by correcting the code and rebuilding from the log. The corrected history appears with no data-repair migration against production rows.

None of it survives a badly designed event, because an `OrderUpdated` payload carrying a status field turns the log back into a diff table that happens to be append-only.

## [Copy link to heading](#core-components-of-an-event-sourcing-system)Core components of an event sourcing system

The 3 components answer the questions any implementation has to settle: where events get written, how current state comes back, and how queries get served without scanning the whole log.

### [Copy link to heading](#the-event-store-and-optimistic-concurrency)The event store and optimistic concurrency

An event store is an append-only database with two operations. It appends events to a stream, and it reads the events for a stream. Events are immutable once written, so corrections are appended as new events, because the value of the log depends on nothing ever being deleted.

Concurrent writers are handled optimistically rather than with locks. When two handlers read the same stream version and both try to append, the store rejects the second write; the losing handler then reloads, re-evaluates, and retries.

### [Copy link to heading](#replay-and-snapshots)Replay and snapshots

Replay rebuilds state by applying events in sequence to a blank starting point. The cost grows with the length of the stream, and it grows on every read rather than once at startup. A snapshot serializes aggregate state at an interval so replay starts there rather than at event zero.

[LMAX](https://martinfowler.com/articles/lmax.html) shows the ceiling of what disciplined snapshotting buys. Its exchange snapshots nightly, and a full restart that loads the snapshot and replays a day of journals finishes in under a minute.

### [Copy link to heading](#projections,-read-models,-and-the-slide-into-cqrs)Projections, read models, and the slide into CQRS

An event log is ordered by time and keyed by stream, which is the wrong index for almost every question a product needs answered. A query like "show all orders above $100 last month" can't be served from a stream of `OrderPlaced` events without reading all of them, so it needs a projection: a materialized view derived from the log and built for one query pattern.

Projections are how the read side gets built, which is why event sourcing and Command Query Responsibility Segregation (CQRS) travel together. Every new query pattern means another projection; each read model lags the log by however long event processing takes; and changing a shared type ripples into every projection that reads it.

## [Copy link to heading](#how-to-run-event-sourcing-on-serverless-with-vercel)How to run event sourcing on serverless with Vercel

Traditional event sourcing implementations assume a long-running process that holds aggregates in memory and runs projections on background threads. A function invocation has neither, so each component needs an explicit home. The log goes to managed storage; the projections run as queue-triggered workers; and the code doing the appending needs a durable execution layer of its own.

### [Copy link to heading](#a-durable-log-for-the-record,-consumer-groups-for-the-projections)A durable log for the record, consumer groups for the projections

A message queue is not an event store, and treating one as the log of record is the most common architectural mistake here. [Vercel Queues](https://vercel.com/docs/queues/concepts) expose durable, append-only topics; every message is synchronously written to 3 availability zones before the publish call returns; but retention is configurable per message from 60 seconds to 7 days, defaulting to 24 hours. Messages are permanently deleted when their retention period expires, regardless of processing state.

The workable division puts a durable store in charge of the log, with Queues carrying events to the workers that build read models. Adding a projection then means adding a consumer group: [new consumer groups](https://vercel.com/docs/queues/poll-mode) always start at the beginning of the topic and the number of groups is unlimited, so a new read model catches up on available history without touching the producer or any other consumer.

### [Copy link to heading](#crash-recovery-without-re-charging-the-card)Crash recovery without re-charging the card

Nothing durable records that the charge already succeeded, so the retry runs it again. The usual workaround is writing progress markers into your own database: an event log built by hand, with none of the replay guarantees.

[Vercel Workflows](https://vercel.com/docs/workflows) apply the pattern to execution state, and have processed over [100 million runs](/blog/a-new-programming-model-for-durable-execution) and over 500 million steps across more than 1,500 teams since the beta launched in October 2025. Every step, input, output, sleep, hook, and error is recorded automatically, and that log is the single source of truth for the run. When a function crashes or a deployment rolls out mid-run, the SDK replays the log to restore state and skips the steps that already completed. Durability comes from two directives rather than a separate orchestration codebase:

```
import { payments } from "@/lib/payments";
import { fulfillment } from "@/lib/fulfillment";

export async function checkout(orderId: string) {
  "use workflow";

  const charge = await chargeCard(orderId);
  const shipment = await createShipment(orderId, charge.id);

  return { charge, shipment };
}

async function chargeCard(orderId: string) {
  "use step";

  return payments.charge({
    orderId,
    idempotencyKey: `charge:${orderId}`,
  });
}

async function createShipment(orderId: string, chargeId: string) {
  "use step";

  return fulfillment.createShipment({
    orderId,
    chargeId,
    idempotencyKey: `shipment:${orderId}`,
  });
}
```

[Vercel Functions](https://vercel.com/docs/functions) execute the workflow and step code; Queues deliver at-least-once; and step handlers therefore need to be idempotent for the redelivery edge cases.

### [Copy link to heading](#determinism-without-wrapping-every-nondeterministic-call)Determinism without wrapping every nondeterministic call

Replay only produces the correct result when the code takes the same path it took the first time. Imagine someone calls `Date.now()` inside workflow logic, everything passes in testing, and the inconsistency only appears after a crash triggers a replay months later. Engines that require every nondeterministic call to be wrapped by hand push that risk onto the developer, and the one call somebody forgets is the one that breaks recovery.

The Workflow SDK seeds these values. [`Math.random()`](https://workflow-sdk.dev/docs/api-reference/workflow-globals) [,](https://workflow-sdk.dev/docs/api-reference/workflow-globals) [`Date.now()`](https://workflow-sdk.dev/docs/api-reference/workflow-globals), and `crypto.randomUUID()` are safe to call directly in workflow functions, because the framework returns the same values across replays. `process.env` is exposed as a frozen, read-only snapshot taken when the run started.

### [Copy link to heading](#an-audit-trail-with-no-projections-to-maintain)An audit trail with no projections to maintain

Compliance review asks queries rather than replays: who changed a setting, when, and from which account. An event log answers that only once somebody builds a projection for it and keeps the projection current.

The [Activity Log](https://vercel.com/docs/activity-log) records team and project changes across 574 documented actions, and [Audit Log Drains](https://vercel.com/docs/drains/reference/audit-logs) forward them to S3, Splunk, Datadog, or Panther. Each event carries the actor, delegation chain, request ID, and IP address, so the record arrives queryable with no read model to rebuild.

## [Copy link to heading](#the-costs-of-event-sourcing)The costs of event sourcing

None of this hurts in a prototype. The bills arrive once the log is long, the event schema has changed a few times, and someone is waiting on a read model to catch up.

### [Copy link to heading](#schema-evolution-never-finishes)Schema evolution never finishes

A relational migration rewrites old rows once and moves on. An event schema can't do that, because replay means every historical event has to remain processable by current code permanently. Old versions never retire and accumulate alongside the new ones.

The usual solution is an upcaster, a translation layer that reads a version-1 event and hands the current code a version-4 form. Upcasters grow a branch per version per event type; they only run during replay; and they are among the least-exercised code in the system, as well as the code most likely to be wrong when it finally executes. Managed execution layers narrow the problem rather than solving it: Workflows [pin each run](https://vercel.com/docs/workflows/concepts) to the deployment that created it, so a run started before a deploy finishes on the code it recorded against.

### [Copy link to heading](#snapshots-trade-storage-for-replay-speed)Snapshots trade storage for replay speed

Snapshots look like free performance until the aggregate's form changes. A snapshot serializes state under one version of the code; a change to that form invalidates every existing snapshot; and they all have to be regenerated from the log before they're usable again.

Interval tuning is the other recurring cost. Snapshot too often and storage grows alongside a log that already grows forever; snapshot too rarely and replay drifts back toward its original cost; and neither limit announces itself until latency degrades in production.

### [Copy link to heading](#projection-lag-reaches-the-screen)Projection lag reaches the screen

Eventual consistency in the read model is a user-experience problem before it's an engineering one. Consider what happens when someone places an order, lands on the dashboard, and sees "processing" because the materialized view hasn't caught up with an event that already happened. The system is correct and the screen is wrong.

Advance agreement on which reads tolerate lag, and which have to be served from the write model directly, avoids a redesign after launch.

### [Copy link to heading](#a-trigger-based-audit-table-covers-most-audit-requirements)A trigger-based audit table covers most audit requirements

Audit logging is the weakest common reason to adopt event sourcing, and, on its own, it almost never justifies the cost. The audit trail most teams want needs none of the projection machinery.

Supabase's [trigger-based audit table](https://supabase.com/blog/postgres-audit) shows the form. A trigger fires on every insert, update, and delete, then writes to a separate audit schema with no changes to application code. Storing the row as jsonb in `record` and `old_record`, rather than mirroring the source table's columns, means turning on auditing doesn't force a matching migration whenever the source schema changes.

## [Copy link to heading](#own-the-log-at-the-right-layer)Own the log at the right layer

A checkout function that charges a card and loses track of the shipment doesn't need its orders table rebuilt as an event store. It needs its execution to be durable. Those are different problems; conflating them is how you end up maintaining projections and upcasters to solve what was always a crash-recovery bug.

Event sourcing is worth the cost for financial ledgers, exchange order books, and durable-execution engines, where reconstructing an arbitrary past state is a core requirement rather than a reporting feature. Most web-application teams aren't building those, and the honest version of the decision is about which layer owns the log, not whether to keep one.

Here's how Vercel covers the parts most teams need:

- **Vercel Workflows:** Execution state is recorded automatically, so a crashed or redeployed function resumes from its last completed step without any instrumentation from the application team.

- **Vercel Queues:** Durable, append-only topics with synchronous replication to 3 availability zones, and unlimited consumer groups that each start at the beginning of the topic, which is what makes a new projection cheap to add.

- **Deterministic replay by default:** Seeded clocks, random sources, and environment variables remove the most common source of replay bugs without hand-wrapping every call.

- **Deployment pinning:** Runs finish on the deployment where they started, which contains the version-skew problem that schema evolution creates in a hand-built event store.

- **Activity Log and Audit Log Drains:** A queryable record of team and project activity, exportable to existing security tooling.

[Start a new Vercel project](https://vercel.com/new) and get a durable execution log from two directives, or browse [vercel.com/templates](https://vercel.com/templates) for a working setup to build on.

## [Copy link to heading](#frequently-asked-questions-about-event-sourcing)Frequently asked questions about event sourcing

### [Copy link to heading](#how-is-event-sourcing-different-from-change-data-capture)How is event sourcing different from change data capture?

Change data capture (CDC) derives events from database row changes after the write has happened, so its events describe structural diffs. Event sourcing captures domain intent at write time, which is why a CDC stream can tell you a status column changed to `cancelled` whereas an event log can tell you the customer cancelled.

### [Copy link to heading](#can-you-use-kafka-as-an-event-store)Can you use Kafka as an event store?

Kafka works well as the transport between the log and downstream projections, and less well as the store itself. It has no built-in optimistic concurrency, so two writers can both append against the same expected version; and it has no API for reading every event for a single aggregate. Log compaction also has to stay off, since keeping only the latest value per key destroys the history the pattern depends on.

### [Copy link to heading](#do-you-need-event-sourcing-for-an-audit-trail)Do you need event sourcing for an audit trail?

No. The deciding question is whether anyone needs to reconstruct full system state at an arbitrary past moment, or only to see who changed what and when. If it's the second, a trigger-based audit table or a managed activity log answers it with none of the schema-evolution burden.

### [Copy link to heading](#how-do-vercel-workflows-use-event-sourcing)How do Vercel Workflows use event sourcing?

Workflows record each step's input, output, and outcome in an event log that is the single source of truth for the run. When a function crashes or redeploys mid-run, the SDK replays that log to restore state without re-running completed steps.