0006-reservation-protocol-posting-lifecycle.md 5.0 KB

Reservation protocol and the posting lifecycle

  • Status: accepted
  • Authors: Cesar Rodas
  • Date: 2026-06-29
  • Targeted modules: kuatia-types (PostingStatus, ReservationId), kuatia-storage, kuatia (saga)
  • Associated tickets/PRs: N/A

Context and Problem Statement

A commit must consume input postings exactly once, even when many commits run concurrently and the commit itself is a multi-step saga (ADR-0002) over a dumb store (ADR-0003) with no global transaction. Two sagas must never both spend the same posting, and a posting reserved by one saga must not be finalized or released by another. How is exclusive, recoverable ownership of inputs achieved without locking account balances?

Decision Drivers

  • Double-spend safety: a posting can be consumed at most once, unconditionally.
  • No hot-row locking: preserve the UTXO concurrency of ADR-0001; do not serialize on an account balance.
  • Survives the saga's lifetime: a reservation must hold across reserve → validate → finalize and across a crash + recovery.
  • Ownership: only the saga that reserved a posting may finalize or release it.
  • Fits dumb storage: expressible as single atomic conditional updates that return affected-row counts (ADR-0003).

Considered Options

Option 1: Database row locks (SELECT … FOR UPDATE) per posting

Lock the posting rows for the duration of the commit.

Pros:

  • Good, because it gives strict mutual exclusion within one database transaction.

Cons:

  • Bad, because it requires a transaction spanning the whole commit, which the saga model deliberately avoids (ADR-0002/0003).
  • Bad, because held locks block other workers and do not survive a crash (the lock is gone, but no record says the posting was claimed).
  • Bad, because it ties the design to a locking, transactional store.

Option 2: Optimistic balance CAS per account

Guard each commit with a compare-and-set on the account balance.

Pros:

  • Good, because it avoids long-held locks.

Cons:

  • Bad, because it serializes on a per-account balance (the hot row ADR-0001 set out to avoid) and conflates "did this posting get spent" with "did the balance change."
  • Bad, because it does not, by itself, record exclusive ownership of specific inputs for recovery.

Option 3: A three-state posting lifecycle with a reservation token

A posting is Active → PendingInactive → Inactive. reserve_postings(ids, rid) flips Active → PendingInactive and stamps each with a ReservationId, as a single atomic conditional update (… WHERE status = Active). release_postings reverts PendingInactive → Active for the owning rid; finalize flips PendingInactive (owned by rid) → Inactive. The reservation id is durable (persisted with the write-ahead record, ADR-0003), and every later mutation is conditioned on ownership.

Pros:

  • Good, because reservation is a single atomic conditional update. Two sagas cannot both move the same Active posting to PendingInactive; the loser sees zero rows affected. Double-spend safety is unconditional and lock-free.
  • Good, because the PendingInactive state plus ReservationId is durable ownership that survives the multi-step saga and a crash, enabling recovery to tell "reserved by us" from "taken by someone else."
  • Good, because it expresses cleanly over dumb storage (counts, not locks) and keeps balances out of the critical section.
  • Good, because compensation is natural: release reverts the reservation.

Cons:

  • Bad, because a posting carries lifecycle state and an optional reservation column (more than an immutable UTXO).
  • Bad, because a reservation orphaned by a crash must be resolved by recovery (roll-forward) rather than by a lock simply being dropped. ADR-0003 handles this.

Decision Outcome

Chosen option: Option 3: the three-state posting lifecycle with a durable ReservationId, because it is the only option that gives unconditional, lock-free double-spend safety and durable, recoverable ownership across a multi-step saga, while expressing as the atomic, count-returning instructions the dumb store provides.

Positive Consequences

  • reserve_postings is the concurrency gate; the saga reads its affected-row count to know it won the reservation (ADR-0003's count interpretation).
  • Recovery distinguishes "reserved by this saga" / "already finalized by us" from "taken by another transfer," which is what makes phase-tracked roll-forward safe (ADR-0003).
  • Balances never enter the critical section. UTXO concurrency is preserved.

Negative Consequences

  • Postings are not pure immutable UTXOs; they carry status + reservation.
  • Crash-orphaned reservations are resolved by recovery, not by lock release.

Links