Inbox (architectural)
The journal of consumed events — so a broker retry does not re-trigger an already-done side effect.
Problem
A broker delivers at-least-once: the same message may be consumed twice if the consumer restarts between processing and offset commit. The business side effect (e.g., billing) must not occur twice.
Forces
- The consumer does not control the broker at-least-once delivery.
- A non-idempotent side effect (external paid API call, accounting file write) must be protected.
- The inbox must have a unique key per message_id (UNB control reference, ISA13).
- The broker commit cannot live in the same transaction as the application DB (except XA).
Solution
Create an `inbox` table (message_id, source, processed_at, status). On consumption, the consumer (1) tries to insert message_id into `inbox`; (2a) if the insert succeeds → execute the business side effect in the same transaction, then COMMIT, then ACK broker; (2b) if the insert fails (unique violation) → message already processed, ACK broker without side effect. The pattern mirrors Idempotent Receiver semantics but at the infrastructure level.
EDI implementation
In EDI, a classic case: the hub consumes an INVOIC arriving from the partner and calls the internal accounting API to create the invoice. Without Inbox, a broker retry doubles the invoice. With Inbox: the first consumption inserts ISA13+sender into `edi.inbox` + creates the invoice in transaction. The retry sees the insert fail, ACKs cleanly without re-invoicing. Implementations: manual pattern on PostgreSQL, or libraries Eventuate Tram, MassTransit Inbox.
Anti-patterns
- Inbox without a unique index on (message_id, source) — the pattern does not work.
- Side effect outside the insert transaction — race condition.
- Inbox without purge — the table grows indefinitely.
- Inbox on all messages indiscriminately — needless overhead for naturally idempotent flows.
Related patterns
- Idempotent Receiver — code version of the same guarantee.
- Outbox (architectural) — mirror pattern on the producer side.
- Transactional Client — local transaction.
- Message Store — the inbox table often doubles as a log.
Sources
- Microsoft Learn — Idempotent message processing. learn.microsoft.com/en-us/azure/architecture/reference-architectures/event-hubs/
- Chris Richardson — Microservices Patterns, Inbox Pattern. microservices.io/patterns/data/transactional-outbox.html