Command Logs and Reconciliation
Part 3 — turning state into an append-only stream of facts, and merging them deterministically.
State is derived, not stored
In the first two posts we sketched the system; now we make it honest. The rule that keeps everything sane: never mutate state directly. State is a pure fold over an append-only log of commands. The log is the source of truth; the UI reads a projection of it.
const commands = [
{ id: "c1", type: "task.create", title: "Ship Part 2", at: "2025-06-29" },
{ id: "c2", type: "task.complete", taskId: "c1", at: "2025-06-29" },
];
const reduce = (state, cmd) => {
switch (cmd.type) {
case "task.create":
return { ...state, [cmd.id]: { title: cmd.title, done: false } };
case "task.complete":
return { ...state, [cmd.taskId]: { ...state[cmd.taskId], done: true } };
default:
return state;
}
};
const state = commands.reduce(reduce, {});
Merging two logs
When two devices come back online, each has its own log. Merging is just a
union of commands, ordered by a deterministic timestamp (say, a hybrid logical
clock). Because reduce is deterministic, both devices arrive at the same
state after folding the merged log.
When commands conflict
Not every conflict is clean. If two devices both edit the same task title, the merge needs a policy. The simplest is last-write-wins by clock:
The expected fraction of "correct" writes under LWW, given independent arrival times uniformly distributed on , is just the chance one arrives last:
LWW is cheap and usually fine for task titles. For rich text, you'd reach for a CRDT instead — that's the next part.
Takeaways
- Keep an append-only command log; derive state by folding it.
- Merging logs is a union; determinism comes from the reducer, not magic.
- Pick the cheapest conflict policy that your data can survive.
More articles
The Sync Engine
Part 4 — the component that carries commands between devices and the canonical server.
The Architecture Of A Local-First Sync System
A high-level walkthrough of the architecture behind a local-first task app: clients, edge, Go services, PostgreSQL shards, outbox, Kafka, projections, and object storage.
Why I'm Building a Todo App Like a Distributed System
A first-principles post on building a local-first task app around operation logs, idempotent writes, sync, and PostgreSQL as canonical state.