Transactions

June 8, 2026 ยท View on GitHub

Transactions coordinate changes across multiple keys. Use them when a workflow must read or write several keys as one logical unit.

For simple high-throughput writes, prefer the non-transactional API.

Open A Transactional Tree

using ZoneTree;

using var zoneTree = new ZoneTreeFactory<int, string>()
    .SetDataDirectory("data/tx")
    .OpenOrCreateTransactional();

Basic Transaction

var tx = zoneTree.BeginTransaction();

zoneTree.Upsert(tx, 1, "first");
zoneTree.Upsert(tx, 2, "second");

var result = zoneTree.PrepareAndCommit(tx);

Console.WriteLine(result.Result);

Prepare and PrepareAndCommit report commit state:

ResultMeaning
Committedthe transaction committed
ReadyToCommitPrepare succeeded and a separate Commit call can finish it
PendingTransactionsthis transaction depends on uncommitted transactions
Abortedthe transaction was aborted

When To Use Transactions

Use transactions for:

  • moving value from one key to another,
  • updating a primary record and secondary index together,
  • ensuring multiple keys change together,
  • checking multiple keys before writing.

Do not use transactions merely because writes are concurrent. Regular writes are designed for concurrent workloads.

Atomic Methods Vs Transactions

RequirementUse
Current value of one key decides the next valueatomic method
Multiple keys must commit togethertransaction
Simple replace/add/deleteregular write API

Keep Transactions Short

Keep transaction work focused. Long-running transactions increase conflict windows and can make other transactions wait or retry.

No-Throw APIs

Most transactional APIs have NoThrow variants. Read/write methods return TransactionResult or TransactionResult<T> instead of throwing TransactionAbortedException; prepare and commit methods return CommitResult.

var tx = zoneTree.BeginTransaction();

var write = zoneTree.UpsertNoThrow(tx, 1, "value");
if (write.IsAborted)
{
    return;
}

var commit = zoneTree.PrepareAndCommitNoThrow(tx);
if (commit.IsPendingTransactions)
{
    Console.WriteLine("Waiting on: " +
        string.Join(", ", commit.PendingTransactionList));
}

Fluent Transactions

BeginFluentTransaction wraps retry handling for aborted and pending transactions.

using var tx = zoneTree.BeginFluentTransaction();

var result = await tx
    .Do(id => zoneTree.UpsertNoThrow(id, 1, "first"))
    .Do(id => zoneTree.UpsertNoThrow(id, 2, "second"))
    .CommitAsync();

Console.WriteLine(result.Succeeded);

The fluent API is useful when retrying a short unit of work is easier than managing prepare, pending dependencies, and rollback directly.

Read-Committed Reads

Transactional trees expose read-committed helpers for reading committed data without starting a new transaction.

if (zoneTree.ReadCommittedTryGet(1, out var value))
{
    Console.WriteLine(value);
}

Pass the current transaction id when a transaction should see its own uncommitted writes and committed writes from others.

Auto-Commit Helpers

UpsertAutoCommit and DeleteAutoCommit start a transaction, perform one write, and commit it. They are convenient when all writes must pass through the transaction log, but they are heavier than regular Upsert and ForceDelete.

Transaction Maintenance

Transactional maintenance exposes the underlying tree and transaction log:

  • zoneTree.Maintenance.ZoneTree,
  • zoneTree.Maintenance.TransactionLog,
  • zoneTree.Maintenance.UncommittedTransactionIds.

Roll back abandoned transactions during operational cleanup:

zoneTree.Maintenance.RollbackUncommittedTransactionIdsBefore(
    DateTime.UtcNow.AddMinutes(-10));