# ADR-007: Transactional seam — caller-held tx handle with `*_tx` methods ## Status Accepted (the seam shape both POCs verified); handle-type ergonomics decided by [ADR-008](008-contract-v1-pinning.md) §2 — the `*_tx` methods live on the handle trait, no downcast ## Context The load-bearing property of the whole store (guiding principle 2) is **transactional local-adjacency**: enqueue/publish/notify inside the same transaction as the caller's business write; rollback drops both. Serving it through an async trait over two transaction models — rusqlite's sync, thread-bound connection vs tokio-postgres's `Send + Sync` client — is the one genuinely driver-coupled design point (OQ-ST-03's framing). Both POCs (2026-10-04) built the candidates and measured them. ## Decision The core contract's transactional seam is a **caller-held transaction handle**: ```text store.begin_tx() -> TxHandle handle.enqueue_tx(name, opts, payload) -> job id handle.publish_tx(name, payload) -> event id handle.notify_tx(channel, payload) handle.save_offset_tx(stream, consumer, offset) handle.commit() / handle.rollback() ``` - The handle is owned by the caller across `await` points; every `*_tx` operation lands in *the caller's transaction*; commit/rollback are explicit and owned by the caller. Non-`_tx` operations are the auto-commit convenience counterparts, each atomic alone. - **Postgres** ([ADR-004]): the handle holds the pooled connection object directly (tokio-postgres `Client` is `Send + Sync` — verified by a compile-time probe). Straight `.await`s, no bridge, no hop cost. Commit/rollback return the object to the pool. - **SQLite** ([ADR-003]): the handle is a **writer-slot lease** — `begin_tx` acquires the `Writer` slot and opens `BEGIN IMMEDIATE`; every handle op round-trips `spawn_blocking` to that connection (rusqlite is not `Send`-across-await / not `Sync`). Holding the slot across `await` points *is* how the lease works — a long transaction parks the single WAL writer, which is SQLite's own shape. - **Closure-scoped transactions** (`with_tx`) are *available as a wrapper over* the handle shape, not instead of it (the POC verified the reverse composition doesn't work: a closure cannot outlive itself, so caching-subscriber state can't escape it). - Both engines deliver the property **natively** — no emulation: in-tx notify/Notify delivers only at commit; rollback drops job rows, business rows, and notifications together (the POC property tests on both engines green: no ghosts). The POC's trait sketch carried two frictions, both now Phase 1 contract-pinning input rather than open risk: 1. The `as_any_mut` downcast per engine for `*_tx` methods on a `dyn TxHandle` — small, but a generic or enum-favored handle may be cleaner. Decided by [ADR-008](008-contract-v1-pinning.md) §2 (with the full contract). 2. Thread-affinity of rusqlite tx ops (SQLite handle ops must round-trip the same blocking thread) — inherent to [ADR-003]'s bridge, documented as a contract note ("SQLite tx ops are serialized by the writer slot"), not a defect. ## Consequences **Positive** - The load-bearing property is one seam, both engines, POC-verified end-to-end — the dual-write problem is structurally prevented, not consumer-disciplined. - Long transactions compose naturally (checkout-then-work shape); nothing about the seam forbids multi-statement business transactions with interleaved reads. - Per-engine bridging differences are invisible to consumer code: the same `*_tx` calls, the same commit/rollback ownership. **Negative** - The SQLite handle parks the writer for its duration — a slow consumer transaction throttles all writers on the file. This is honest (WAL single-writer), but contract docs must say it so consumers budget transactions accordingly. - Per-op `spawn_blocking` hop on SQLite tx ops (measured fine at ~0.35 ms p50; the dedicated-thread bridge is the recorded optimization). - `with_tx` wrapping is a convenience surface we must ship and test so it doesn't accrete ad-hoc in consumer code. ## References - `docs/research/poc-sqlite-posture-findings.md` §Arm A, §Probe 1/3; `docs/research/poc-pg-posture-findings.md` §Sub-module T. - OQ-ST-04 (`docs/research/phase-0.md`), the `*_tx` seam bullet. - [ADR-003](003-sqlite-driver.md), [ADR-004](004-postgres-driver.md) — the per-engine handle mechanics. - [ADR-006](006-wake-and-delivery-contract.md) — commit-atomicity is the `*_tx` property this seam carries into the notify contract. - [core-contract.md](../core-contract.md) — the spec whose transaction section this ADR defines. [ADR-003]: 003-sqlite-driver.md [ADR-004]: 004-postgres-driver.md