Transactional Inbox
A transactional inbox stores received messages in the same database transaction as their processing, so consumers handle each message exactly once even under redelivery.
Definition
The transactional inbox (consumer-side dual of the outbox) persists each consumed message id in the same transaction as its business effect. Redelivered messages find the recorded id and are skipped as already processed. It converts at-least-once delivery into effectively-once processing without distributed transactions, at the cost of a durable deduplication table per consumer.
Context and scope
Applied in Kafka consumers, SQS handlers, and webhook processors — anywhere redelivery overlaps with non-idempotent side effects. Pairs with the transactional outbox on the producer side.
Common misconceptions
- Idempotent consumers make an inbox unnecessary — designed idempotency covers specific operations; the inbox covers all of them uniformly, including side effects you forgot to make idempotent
- The inbox must be a separate service — it is a table in the consumer database, written inside the same transaction as the effect
- Once implemented, ordering is solved too — the inbox deduplicates delivery; ordering requires key-based partitioning or sequencing on top
Anti-patterns
- Marking messages processed before the business write commits and losing them on rollback
- Growing the inbox forever without archival and slowing every transaction
- Deduplicating on broker offset instead of message id and breaking under rebalances and reprocessing
Related concepts
Authoritative source
https://microservices.io/patterns/data/transactional-outbox.html
What it is
Brokers redeliver: crashes, timeouts, rebalances. Every consumer that performs non-idempotent work will eventually execute a message twice. The inbox records what was already done, atomically with doing it.
How it works
On receive, the handler transaction inserts the message id into the inbox table and performs the business write. A redelivery hits a unique violation on the id and skips. Cleanup policies archive or partition the inbox by age so the hot table stays small.
When to apply
Consumers writing to databases with non-idempotent effects: double charges, duplicate records, double notifications. Combined with the outbox, it yields end-to-end effectively-once flows without distributed transactions.
Pitfalls
Dedup key choice matters: broker offsets change on replay and rebalance, while business or producer message ids survive them. The inbox write must share the business transaction — a separate commit window loses messages or double-processes. Non-database side effects — API calls, emails — still need idempotency keys at the target, because the inbox only protects the local transaction.