Persistence: state, events and replay
Three ways to make data and work survive a restart: store the current state, store every change as an event, or store a program’s progress and replay it.
3 technologies on this page ↓How it works
The common way is to keep the current state: one row per thing, overwritten on every change, so a read is a lookup but the old values are gone. Event sourcing keeps the changes instead: every change is an immutable event appended to a log, and the current state is computed by replaying an entity’s events in order, from a snapshot when the stream is long. Durable execution applies the same idea to a running program: an engine appends each step the code takes, and its result, to an event history. After a crash, the code runs again from the top, and every step already in the history returns its recorded result instead of running twice, until the program is back where it stopped.
A bank balance, a bank statement and a recipe with ticked steps. The balance tells you only what you have now. The statement lists every transaction, so you can add it up to any date. The ticked recipe lets a new cook take over and start at the first unticked step.
Reach for it when
- State persistence: most applications, where only the current value matters and reads must see the latest write
- Event sourcing: you need a full audit trail, the state as of any past moment, or several read models built from the same facts
- Durable execution: multi-step processes that must finish despite crashes and restarts, such as payments, orders or anything that waits days for a reply
Think twice when
- Event sourcing for plain CRUD, prototypes or reference data: event design, versioning and projections cost more than they return
- Event sourcing when every view must be up to date at once: projections are eventually consistent
- Durable execution for workflow code you can’t keep deterministic, since every replay must make the same calls in the same order
- Any append-only store for personal data you must be able to erase, unless you plan for it up front (for example by encrypting it with a key per person and deleting the key)
A real example
An account receives a deposit of 100, a withdrawal of 30 and a deposit of 5. With state persistence the database holds one row: balance 75. With event sourcing it holds three events; adding them up gives 75, and stopping after the second gives 70, the balance at that moment. A durable transfer workflow first debits one account, then credits another. If its worker dies between the two, another worker re-runs the code: the debit’s result is already in the history, so it is returned rather than repeated, and only the credit runs.
Kinds of persistence: state, events and replay
Specific products, and what each does differently.
State persistence →
Keep only the current value of each record, and overwrite it in place whenever it changes.
Event sourcing →
Store every change as an immutable event in an append-only log, and compute the current state by replaying the events.
Axon FrameworkOpen sourceA Java framework, not a database, for building event-sourced applications with CQRS and domain-driven design: commands, events and queries as messages, plus an event store to keep the events in.Durable execution →
Persist a running program’s progress as an event history, so after a crash the code is replayed against that history and carries on from where it stopped.
Axon Framework | Event sourcing | Open source | Axoniq | Apache 2.0; the Axoniq Framework modules on top of it are commercial and need a subscription for production | 2026-10-08 |
| Event sourcing | Commercial product | Kurrent | Kurrent License v1 (source available, not OSI open source) since 25.0 in March 2025; Event Store License v2 for 24.10; a BSD 3-clause license for earlier versions | 2026-10-08 | |
| Durable execution | Open source | Temporal Technologies | MIT | 2026-10-05 |
The three approaches side by side#
| State persistence | Event sourcing | Durable execution | |
|---|---|---|---|
| What is stored | The current value of each record | Every change, as an immutable event | Each step a running program takes, with its result |
| How you get the current state | Read the row | Replay the entity’s events, from a snapshot if there is one | Re-run the code against its history |
| History | Lost on each update, unless you add audit tables | Complete: rebuild the state as of any event | Complete for each execution, within the engine’s limits (Temporal: 51,200 events or 50 MB) |
| How a write happens | Update in place, usually inside a transaction that locks the row | Append, checking the stream is still at the version you read | The engine appends as your code calls its SDK |
| The main rule you live with | None beyond the schema | Events are never edited: correct mistakes with new events, version their schema and accept eventually consistent read models | Workflow code must be deterministic; side effects go in activities |
| Examples | A table in PostgreSQL, MySQL or SQLite | KurrentDB, Axon Framework, or an append-only table | Temporal, Azure Durable Functions, Restate |
State and events in plain SQL#
The same account kept both ways, runnable in SQLite or PostgreSQL. The primary key on (account_id, version) acts as the optimistic concurrency check: of two writers that read version 1, only the first can append version 2.
-- State persistence: one row, overwritten
CREATE TABLE accounts (id INT PRIMARY KEY, balance INT NOT NULL);
INSERT INTO accounts VALUES (42, 100);
UPDATE accounts SET balance = balance - 30 WHERE id = 42;
SELECT balance FROM accounts WHERE id = 42; -- 70, and the old 100 is gone
-- Event sourcing: append only, one version number per event
CREATE TABLE account_events (
account_id INT NOT NULL,
version INT NOT NULL,
type TEXT NOT NULL,
amount INT NOT NULL,
PRIMARY KEY (account_id, version)
);
INSERT INTO account_events VALUES (42, 1, 'Deposited', 100), (42, 2, 'Withdrawn', 30);
SELECT sum(CASE type WHEN 'Deposited' THEN amount ELSE -amount END)
FROM account_events WHERE account_id = 42; -- 70, rebuilt from the events
SELECT sum(CASE type WHEN 'Deposited' THEN amount ELSE -amount END)
FROM account_events WHERE account_id = 42 AND version <= 1; -- 100, the balance as of version 1
-- A second writer that also read version 1 tries to append version 2: rejected
INSERT INTO account_events VALUES (42, 2, 'Withdrawn', 50); -- error: duplicate primary keyIn code · python
from functools import reduce
events = [
{"type": "Deposited", "amount": 100},
{"type": "Withdrawn", "amount": 30},
{"type": "Deposited", "amount": 5},
]
def apply(balance, event):
if event["type"] == "Deposited":
return balance + event["amount"]
if event["type"] == "Withdrawn":
return balance - event["amount"]
return balance
print(reduce(apply, events, 0)) # 75: the current state is a fold over all events
print(reduce(apply, events[:2], 0)) # 70: the state as it was after the second eventOften confused with
Where it came from2005–2020
Keeping every change is older than computers. As Pat Helland puts it, “accountants don’t use erasers”: a ledger keeps every entry, and a correction is a new entry. Databases work the same way inside. Their transaction logs record every change, and in Helland’s words “the truth is the log. The database is a cache of a subset of the log.” Applications, though, mostly kept only that cache: the latest value of each row.
In December 2005 Martin Fowler described Event Sourcing: capture every change to an application’s state as an event, so you can throw the state away and rebuild it, ask what it was at any past time, or replay corrected events. He also noted the catch that still matters: a replay mustn’t send messages to external systems a second time. Greg Young’s CQRS Documents (2010) tied the pattern to domain-driven design and CQRS, using an accounting ledger as the model and “rolling snapshots” to keep replays short, and in September 2012 Young released Event Store, an open-source database built for it, which became today’s KurrentDB.
Durable execution took the idea from data to running code. Samar Abbas built the open-source Durable Task Framework at Microsoft, whose orchestrations record their actions in an append-only history and replay the code against it; Azure Durable Functions, built on it, became generally available in May 2018. Abbas and Maxim Fateev went on to build Cadence at Uber and then Temporal, which reached 1.0 in September 2020.
Why it’s built this way
- Store the facts, derive the state
- If every change is kept, the current state can always be recomputed, and so can any past state. Young points to the ledger: the running balance can be trusted because the transactions can be re-run from the beginning to check it.
- Never edit or delete an event
- The log is the audit trail, so a mistake is corrected by a new event that reverses the old one (Young’s “reversal transaction”). That keeps history honest, but it collides with laws that require personal data to be erased, which has to be designed for from the start.
- Snapshots as a cache, not the truth
- Replaying a long stream is slow. A snapshot of the state at some point lets you replay only the newer events, and because it can always be regenerated from the events, it never becomes a second source of truth.
- Replay code against recorded results
- Durable execution engines don’t serialise a program’s memory. They re-run the code and feed it the recorded results, which makes crash recovery possible but requires the code to be deterministic.
Timeline
- 2005
Martin Fowler describes Event Sourcing
- 2010
Greg Young’s CQRS Documents link event sourcing, DDD and CQRS
- 2012
Event Store, an open-source event database, is first released
- 2015
Pat Helland’s “Immutability Changes Everything”: the truth is the log
- 2018
Azure Durable Functions, built on the Durable Task Framework, become generally available
- 2020
Temporal 1.0 ships
- Pat Helland: Immutability Changes Everything (CIDR 2015)
- Martin Fowler: Event Sourcing (Dec 2005)
- Greg Young: CQRS Documents (2010)
- InfoQ: Greg Young on using complex event processing (Jan 2014), Event Store first released September 2012
- Azure Architecture Center: Event Sourcing pattern (compensating events, personal data)
- Microsoft Learn: Durable orchestrations (event sourcing in the Durable Task Framework)
- Azure blog: Break through the serverless barriers with Azure Functions (May 2018), Durable Functions GA
- Temporal blog: Building resilient workflows, from Azure to Cadence to Temporal (Apr 2025)
- Temporal blog: Temporal V1 announcement (Sep 2020)
- Kurrent docs: Upgrade guide for KurrentDB 25.0 (EventStoreDB renamed KurrentDB)
Sources11
- Azure Architecture Center: Event Sourcing pattern
- Martin Fowler: Event Sourcing (Dec 2005)
- Greg Young: CQRS Documents (2010)
- PostgreSQL docs: Trigger functions (audit trigger example)
- Microsoft Learn: Durable orchestrations (replay and orchestration history)
- Microsoft Learn: Orchestrator function code constraints
- Temporal docs: Events and Event History
- Temporal docs: Workflow Definition (determinism)
- Temporal docs: Workflow Execution limits (51,200 events or 50 MB)
- Restate docs: Key concepts (durable execution)
- Kurrent docs: Concepts (event log and streams)