Skip to main content
Take a concrete problem: keep a balance per account, reachable over HTTP, correct when requests overlap, and intact across restarts. A debit must never overdraw, and a committed debit must never be lost. Below is what that actually requires, from first principles, and how a stateful actor hands you the same thing.

How You’d Build This, from First Principles

Strip it to the metal. One process serves every account — a, b, c, x, y, … — holding them all in an in-memory map and handling a request for whichever one comes in. Two things make each debit correct: a per-account lock, so two debits on the same account can’t interleave (while debits on different accounts still run in parallel), and a write-ahead log + fsync, so a committed debit is on disk before you reply.
Durability is append-to-log then fsync — you don’t acknowledge the debit until the bytes are on disk:
A request names an account; the process looks it up and serves it. Lock that account, check-and-decrement, log, fsync, unlock:
Two primitives do all the work: the lock is the serialization point, and fsync before you reply is durability. Everything else follows:
  • one process owns it all — the map, the per-account locks, and the log are shared by every account ay;
  • durability is the log: a debit is committed once its record is fsync’d, so a crash loses only what was never acked;
  • recovery is not replaying an ever-growing log from zero — that gets slower forever. You periodically write a snapshot of all balances and keep only the log records since it; on restart you load the snapshot and replay that short tail. (Postgres’ checkpoint + WAL, SQLite’s WAL mode, and Redis’ RDB + AOF all do exactly this.) The on-disk snapshot + log is the truth; memory is a cache;
  • to grow past one process you shard accounts across processes and machines, routing each account to the one that owns it — and this is where most people stop hand-rolling and reach for a database, which is exactly this machine (sharded, replicated, snapshot + write-ahead log) run by someone else. Postgres’ COMMIT is what persist_data does, its checkpoint is your snapshot, and SELECT … FOR UPDATE is the lock.
You can build all of this. The work isn’t any single line — it’s that you own the lock discipline on every path, the log format, the snapshots, the recovery, the sharding, and the restarts, forever.

The Same Thing as a Stateful Actor

The class is only the logic half. In the C version, debit(acct_id, …) did the lookup itself (lookup_or_create) and the logic. With an actor that lookup moves to the caller: a normal function parses the request, names the account with idFromName, and calls the method — which now takes only amount and never has to find its own state.
idFromName(acct) is the actor-world lookup_or_create — instead of finding a struct in this process’s map, it routes to the one instance that owns that account, anywhere (the “Reaching one account” row below). Same check-and-decrement, same durability requirement — but no pthread_mutex, no fsync, no log, no snapshots, no shard map. The two primitives are still there; the runtime provides them. Here’s the mapping, mechanism by mechanism: The runtime takes the one process that multiplexed a, b, c, x, y and splits it: each account id becomes its own instance. idFromName("a") and idFromName("b") are two different instances, on two different threads, each with its own storage — debits on a serialize, debits on a and b run in parallel, and there is no shared map, per-account lock, or single log to own. A stateful actor is that machine — a single-threaded server with durable, flush-before-ack storage, one per account — that the platform shards, routes, and restarts for you. You write the body of debit; the runtime is the lock, the durability, the recovery, and the router. The four guarantees the runtime makes from this mapping are stated precisely in Execution model.

Next Steps