C.W.K.
Stream
Lesson 05 of 05 · published

One Writer, Atomic Audit

~11 min · sqlite, wal, transactions, audit

Level 0Open Gate
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The change and the record of the change must live or die together. If one can happen without the other, you'll eventually get exactly that."

The storage layer's promises

Underneath the snapshot model sits SQLite, configured for durability over throughput: WAL (write-ahead logging), full synchronous writes, foreign keys on, and a busy timeout. And one architectural choice above the database: exactly one application worker owns all writes. Not a pool of writers racing for the same rows — a single writer, so there is never a question of who wrote last. For a single-family, local-first app, throughput was never the constraint; correctness under crash was, and every one of these settings buys correctness.

Why a single writer simplifies everything

SQLite allows many readers but serializes writers, and Keep leans all the way into that: one process, one writer. This dissolves an entire category of concurrency bug. There's no write-write race, no lost update from two workers reading-then-writing the same row, no need for optimistic-locking version columns on every table. The busy timeout handles the rare reader/writer overlap gracefully. Concurrency you don't have is concurrency you don't have to defend against — and at family scale, you genuinely don't need it.

Don't buy concurrency you don't need; it's paid for in complexity forever. Multiple writers are a real requirement at real scale — and a self-inflicted wound below it. A single writer turns a distributed-systems problem into a straight line of code. Match the concurrency model to the actual load, not to the architecture diagram you imagine you'll need someday.

The atomic audit — the part that really matters

Here's the invariant with teeth: every state mutation writes a before/after audit row in the same transaction as the change itself. Not "the change, then an audit log soon after." The same transaction. Either both the mutation and its audit commit, or neither does. This means the audit trail can never disagree with reality: there is no window where a value changed but the record of the change is missing, and no window where an audit says something changed that didn't. They're welded together by the transaction boundary.

If it's not in the same transaction, it's not really an audit. A logging call after the write looks like an audit but has a gap — the process can die in between, leaving a change with no record or a record with no change. The only trustworthy audit is one that commits or rolls back atomically with the thing it audits. Same transaction, or it's just hopeful logging.
WAL and full synchronous are the crash-safety pair. WAL lets readers keep reading while a write is in progress and makes commits durable via the log; full synchronous tells SQLite not to lie about when data hit the disk. Together they mean a power cut mid-write leaves you with a consistent database — the last committed state, never a half-written row.

Code

Mutation and audit share one transaction (illustrative)·python
def set_cash(owner, new_balance):
    with db.transaction() as tx:              # single writer, one atomic unit
        before = tx.fetch_cash(owner)
        tx.update_cash(owner, new_balance)
        tx.insert_audit(                       # SAME transaction as the change
            table="cash", action="update",
            before=before, after=new_balance,
        )
    # Commit is all-or-nothing: the new balance and its audit row
    # both land, or neither does. There is no in-between state where
    # the money changed but the record of why is missing.

External links

Exercise

Take a mutation in any system you've built that also writes a log or audit entry. Are they in the same transaction, or is the audit a separate call after the write? Describe the exact failure (a crash, an exception, a timeout) that would leave the change committed but the audit missing — or vice versa. Then rewrite it so both commit atomically together.
Hint
The gap is almost always 'do the write, then log it.' Kill the process between those two lines and you have a change nobody can explain, or an audit for a change that rolled back. Wrap both in one transaction and the gap physically cannot exist.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.