gobridge

0005 — Outbox partition design: claim selection, fence rows, seq allocation

Status: accepted Date: 2026-07-03 Deciders: GoBridge core

Context

The durable outbox drains records in order and must not lose or reorder them across three backends: in-memory, SQLite, and DynamoDB. Two behaviors need a cross-backend contract:

A relational backend answers “oldest N pending” with ORDER BY created_at, seq LIMIT N. DynamoDB cannot: a Query returns items in sort-key order, and the outbox sort key is OUTBOX#<envelope_id>#<binding_id> — lexicographic by envelope ID, effectively random with respect to record age (adapters/aws/store/dynamodboutbox/acl_store.go). Claiming the first N items DynamoDB returns would starve records whose envelope IDs sort late and break the ordering the other backends provide.

Decision

Fix an ordering contract on the port, then accept a bounded approximation in the DynamoDB backend, with the exact-ordering upgrades recorded as evidence-gated future work.

Consequences

Deferred upgrade paths (future work, evidence-gated)

All are gated on OutboxClaimConflicts / scan-page / age-skew evidence — none is implemented, and none should be until the metrics show it is warranted.

  1. Strongly consistent age-ordered read for keyed partitions. The ClaimIndex GSI already gives keyless partitions the O(limit) read. Keyed partitions fall back to the exhaustive ConsistentRead scan because a GSI cannot answer “is there an older sibling I have not seen”. A local secondary index on (PK, claim_sort) read with ConsistentRead: true would give them the same bound — gated on a table migration (an LSI can only be created with the table) and on the 10 GB item-collection limit per partition key. Until then, a keyed partition with a deep backlog is the case to watch: DynamoDBOutboxClaimScanPages rising on a table that HAS ClaimIndex is the signal.

  2. Age-ordered query (historical). Add a created_at range key (or a per-partition created_at GSI) so the query returns records in age order, and shrink retention from claimRetentionFactor * limit to limit. This removes the O(backlog) per-Claim SCAN COST — the scan can stop after limit instead of reading the whole partition — at the cost of an extra index to write and migrate. Any such index must be provisioned by CreateTable/EnsureTable and verified by the factory schema preflight, never hand-provisioned — see the DynamoDB outbox table schema runbook.

  3. Resharded seq allocation. Split the single per-partition seq_counter into shards to cut fence-row contention on hot partitions. Reduces TransactionConflict at the cost of more complex ordering reconstruction.

Rejected alternatives