Cloud Parts

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 ↓
Live model
What you’re watchingThree lanes, three ways to persist. In the first, each change overwrites the one row. In the second, each change is appended to the event store, and two readers move through the same events: one rebuilds the entity’s state, the other updates a read model. In the third, every finished step goes into the workflow’s history; when worker 1 crashes, the next steps go to worker 2, which replays that history to pick up where worker 1 stopped.

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.

Think of it as

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.

The three approaches side by side#

State persistenceEvent sourcingDurable execution
What is storedThe current value of each recordEvery change, as an immutable eventEach step a running program takes, with its result
How you get the current stateRead the rowReplay the entity’s events, from a snapshot if there is oneRe-run the code against its history
HistoryLost on each update, unless you add audit tablesComplete: rebuild the state as of any eventComplete for each execution, within the engine’s limits (Temporal: 51,200 events or 50 MB)
How a write happensUpdate in place, usually inside a transaction that locks the rowAppend, checking the stream is still at the version you readThe engine appends as your code calls its SDK
The main rule you live withNone beyond the schemaEvents are never edited: correct mistakes with new events, version their schema and accept eventually consistent read modelsWorkflow code must be deterministic; side effects go in activities
ExamplesA table in PostgreSQL, MySQL or SQLiteKurrentDB, Axon Framework, or an append-only tableTemporal, 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.

sql
-- 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 key

In 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 event

Often 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

  1. 2005

    Martin Fowler describes Event Sourcing

  2. 2010

    Greg Young’s CQRS Documents link event sourcing, DDD and CQRS

  3. 2012

    Event Store, an open-source event database, is first released

  4. 2015

    Pat Helland’s “Immutability Changes Everything”: the truth is the log

  5. 2018

    Azure Durable Functions, built on the Durable Task Framework, become generally available

  6. 2020

    Temporal 1.0 ships

Sources11

Improve this page on GitHub