Skip to content
~/docs/internals/mvcc
DOCUMENTATION

MVCC & Transactions

Concurrent access and isolation

KiteDB supports concurrent transactions using Multi-Version Concurrency Control (MVCC). Multiple readers can access the database simultaneously without blocking each other or writers.

Snapshot Isolation

Each transaction sees a consistent snapshot of the database as it existed when the transaction started. Other transactions' uncommitted changes are invisible.

Snapshot Isolation Timeline

T1 startssees v1
T2 startssees v1
T1 commitswrites v2

T2 still sees v1 — T1's changes are invisible until T2 restarts

Version Chains

When data is modified while readers exist, KiteDB keeps old versions in a chain:

Version Chain for Node "alice"

v3: age=32
commitTs=150
v2: age=31
commitTs=120
v1: age=30
commitTs=80
T3 sees this
startTs=145
T2 sees this
startTs=115
T1 sees this
startTs=75

Each transaction follows the chain to find the version committed before it started.

Visibility Rules

Visibility Rules

A version is visible to transaction T if:

1
version.commitTs <= T.startTs

Version was committed before T started

OR
2
version.txid == T.txid

T created this version itself (read-your-own-writes)

Walk the chain from newest to oldest.

Return first visible version.

If none visible → entity doesn't exist for this transaction.

Write Conflicts

KiteDB uses First-Committer-Wins to handle conflicts:

First-Committer-Wins

Scenario: Both T1 and T2 modify "alice"

T1starts at ts=100
T2starts at ts=105

T1 commits first (ts=110)

Succeeds — no conflict

T2 tries to commit (ts=115)

Was "alice" modified after T2.startTs (105)? Yes!

Rolled back with ConflictError

Resolution: T2 must retry with fresh read

typescript
// Handling conflicts
try {
  await db.transaction(async () => {
    const alice = await db.get(user, 'alice');
    await db
      .update(user, 'alice')
      .setAll({ age: alice.age + 1 })
      .execute();
  });
} catch (e) {
  if (e instanceof ConflictError) {
    // Another transaction modified alice
    // Retry with fresh data
  }
}

Lazy Version Chains

Version chains are only created when necessary. If there are no concurrent readers, modifications happen in-place without versioning overhead.

Lazy MVCC Optimization

When T1 modifies "alice":

IF

No other transactions are active

→ Modify in-place (no version chain)

ELSE

Other transactions are active

→ Create version chain (preserve old value)

Result: Serial workloads have zero MVCC overhead. Concurrent workloads get correct isolation.

Garbage Collection

Old versions are cleaned up when no transaction can see them:

Garbage Collection

1Track oldest active transaction (minStartTs)
2For each version chain:
Keep versions where commitTs >= minStartTs
Delete older versions (no one can see them)

Triggered:

  • • After transaction commits
  • • Periodically in background

Note: Long-running transactions delay GC and hold memory.

Transaction API

typescript
// Explicit transaction
await db.transaction(async (ctx) => {
  const alice = await ctx.get(user, 'alice');
  await ctx.update(user, 'alice').setAll({ age: alice.age + 1 }).execute();
  // Commits on successful return
  // Rolls back on exception
});

// Batch operations (single transaction)
await db.batch([
  db.insert(user).values({ key: 'bob', name: 'Bob' }),
  db.insert(user).values({ key: 'carol', name: 'Carol' }),
]);

// Without explicit transaction: each operation is auto-committed

Next Steps