# The commit / write path — reasoning
> **Class:** `reasoning` · **Function:** `commit`. The descriptive *what* is in
> [`commit-design.md`](commit-design.md).
## Why group-commit — the one place embedded genuinely wins
A REST/JVM catalog serialises commits behind a network round-trip and, typically,
a per-commit durable write. An embedded catalog can do something a service
structurally cannot: coalesce N concurrent commits into **one** redb transaction
and **one** fsync. Under a write burst this beats a service on *both* throughput
(fewer fsyncs) and tail latency (no queue behind a socket). This is why
`group_commit` exists and why the `commit_burst` benchmark exists — to demonstrate
the lever, not just claim it. The alternative, one-txn-per-commit, is simpler but
throws away the batching win precisely when it matters most.
The design constraint that makes coalescing safe: each queued `CommitFn` checks
its **own** optimistic precondition and writes nothing on `Err`. So one op's
conflict can't taint the shared transaction — a failing commit in the batch fails
alone, and the rest still land. Without that isolation, batching would couple
unrelated writers' fates.
## Why optimistic CAS instead of a lock held across the commit
We validate requirements and stage metadata *before* taking the redb write lock,
then compare-and-swap the pointer against the base location we read at stage time.
The alternative — hold a lock across "read current → write blob → advance" — would
serialise the expensive object write (network I/O) under the lock, destroying the
group-commit win and adding head-of-line blocking. Optimistic CAS keeps the
network write outside the lock; the only thing under the lock is the pointer
compare-and-swap, which is why many writers can share one transaction. The price
is that a losing writer must retry (`CatalogCommitConflicts` is marked retryable),
which is the right trade when conflicts are rare.
## Why `commit_table` and `atomic_release_raw` exist at all
iceberg-rust 0.9.1 makes `TableCommit`'s builder `pub(crate)`. A caller that holds
the wire pieces — a vec of `TableRequirement` and a vec of `TableUpdate`, e.g.
decoded from an Iceberg-REST `commit_table` request — literally *cannot* construct
a `TableCommit` to hand to the trait's `update_table`. Rather than wait on
upstream, skade exposes `commit_table` (one table) and `atomic_release_raw` (many)
that take the raw triples and drive the identical persist path. When upstream
opens the builder, these stay as convenient lower-level entry points.
## Why multi-table atomicity is redb's job, not ours
Iceberg's per-table `Transaction::commit` guarantees atomicity for one table only.
When several tables represent one logical release (`bench_runs` + `dep_graph` +
`components`), partial visibility of a half-committed set is a real hazard. We get
all-or-nothing across tables for free because redb's write transaction is globally
atomic across every table in the file — so `atomic_release` stages all blobs, then
flips every pointer in a single redb txn. Building this on a filesystem or a
service would have meant a two-phase-commit protocol; redb hands it to us.
The honest edge: the staged **metadata blob writes** (step 2) are not
transactional — a failure there leaves orphan files. We accept this because
Iceberg already has an orphan-file cleanup story, and the alternative (transacting
object writes) isn't available on plain object storage anyway.
## Why durability is a knob, not a fixed policy
`Immediate` (fsync per commit) is the safe default. But bulk import and
benchmarking are legitimately willing to trade crash-durability for throughput,
and redb exposes `Eventual` / `None` levels cleanly. Making it a build-time knob
lets the same catalog serve both a crash-safe production path and a fast
import/bench path without a code fork. The `None` caveat (the file grows because
redb only frees pages at higher durability) is documented so nobody leaves a
production catalog on `None`.