ADR-0004: Postgres only (local ledger, SKIP LOCKED work queue, outbox)
Status: Accepted · Date: 2026-10-19
Context
Async work here is low volume and per payment: resolve unknown outcomes, reverse, capture, release. Correctness depends on the state change and its follow-up job being committed atomically.
Decision
- A single Postgres holds the ledger, the idempotency keys, a
work_itemqueue and anoutbox. - Workers claim jobs with
UPDATE … WHERE id IN (SELECT … FOR UPDATE SKIP LOCKED LIMIT n).UNIQUE(kind, payment_id)makes enqueue idempotent. Retries use exponential backoff with full jitter, then aDEADstate that raises an alert. - The outbox is written in the same transaction as each state change. A Kafka relay (Debezium or a polling publisher) is deferred until a consumer exists (OQ-5).
Consequences
- + No dual write: state and job commit together. One dependency to run, test (Testcontainers) and operate.
- + A partial index keeps claim cost proportional to the backlog.
- − Throughput ceiling far above this workload. Revisit if work items exceed ~1k/s.
- − Watch for queue-table bloat: keep transactions short, and archive
DONErows.