Skip to content

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_item queue and an outbox.
  • 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 a DEAD state 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 DONE rows.