ACID on a single primary

On one primary a graph store gives full ACID by writing changes to a durable write-ahead logical log before commit and holding write locks until commit (detecting deadlocks with a wait-for graph), which is correct and simple but caps write throughput at a single machine — the reason distribution becomes tempting.

Previously

A lock was just one ingredient; the full promise is ACID — a logical log that survives crashes, write locks held to commit, deadlock detection on a wait-for graph. One primary gives you all of it, cleanly. The catch is in 'one primary': every write funnels through a single machine. To go bigger, we have to split the graph — and that's where everything we've built starts to hurt.

Scene 10

ACID on a single primary

  1. Watch
  2. Try it
  3. Predict
  4. Capture
TRANSACTION TIMELINET1RUNNINGBEGIN🔒node 1+FOLLOWS Bob🔒rel 12+FOLLOWS Carol🔒rel 13COMMIT🔒node 1time →WRITE-AHEAD LOGICAL LOGbounded by one primaryWAIT-FOR GRAPH · deadlock detectionedge: “waits on lock held by”no cycle — all can proceedno waiting transactionsT1 appends each change to the write-ahead log and takes a write lock — before it commits.
What to watch for

Watch one transaction. Alice follows Bob and Carol — two relationship inserts that must commit together. Before T1 touches the store, it appends each change to a write-ahead log and takes a write lock on the node it mutates. Then the power fails MID-commit — the COMMIT marker never reaches disk. On restart, recovery replays only the entries that were made durable, so the half-finished write simply vanishes and the graph is back to a consistent state. The change either fully happened or it didn't.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Transaction.commit
log every change durably, THEN write the commit marker
def commit(txn):
for change in txn.changes: # the multi-edge write
log.append(change) # write-ahead: log first
log.fsync() # make the changes durable
log.append(COMMIT, txn.id) # the commit marker
log.fsync() # durable commit = it survives
apply_to_store(txn.changes) # only now touch the store
release_locks(txn) # held until commit
LockManager.acquireWrite
one exclusive write lock per node, held until commit (isolation)
def acquire_write(txn, node):
while node.write_lock.held_by_other(txn):
txn.wait_for(node.write_lock.owner) # edge in wait-for graph
block_until_freed_or_aborted() # may be the deadlock victim
node.write_lock.grant(txn) # exclusive: no interleaving
txn.held_locks.add(node) # held until commit/abort

Where this sits in Build a graph database (Neo4j / Dgraph-style)

Scene 10 of 16, in the Scale & ACID act — ACID on one box; the partition cut turns hops to RPCs.. A single-primary graph store gives full ACID — a write-ahead logical log for durability, write locks held to commit, and deadlock detection via a wait-for graph — at the cost of write throughput bounded by one machine.

Up next. ACID on one machine is clean because every pointer lives in the same address space. The moment the graph outgrows one box and we split it, the value of a graph IS the edges between keys — so any cut we make will sever edges, and a hop across the cut stops being a pointer dereference and becomes a network round trip. You cannot shard a graph the way you shard a key-value store.

All 16 scenes in Build a graph database (Neo4j / Dgraph-style) · Every curriculum

Built with Arqly
Every scene in Build a graph database (Neo4j / Dgraph-style) builds on the one before it.All 16 Build a graph database (Neo4j / Dgraph-style) scenes