The warehouse has accepted a fulfilment command, but its acknowledgement never reaches the broker. The message arrives again. That is not necessarily a publishing bug: redelivery is a common way for reliable transports to avoid losing work.
A Transactional Inbox gives the receiver a way to recognise work it has already performed. The tricky part is not remembering message IDs; it is keeping that memory consistent with the business change.
The record of processing must be atomic with the change
Suppose a consumer checks whether it has seen an event, changes an order, and only afterwards writes “processed”. If the process crashes after the order update but before the marker, a redelivery can repeat the business effect.
For changes inside the consumer's own database, the processing record and the state update should share a local transaction. Either both commit or neither does. A unique message identifier constraint can also protect against concurrent duplicate deliveries.
Transport identity versus business identity
Now imagine the same business command is published again under a new message ID. An Inbox that only deduplicates transport identities will not recognise the underlying operation as the same one.
Important effects may need a separate business key: a payment attempt ID, order ID or another stable identifier. Message deduplication and business-level idempotency solve related but different problems.
What if the consumer calls an external provider?
A local database transaction does not automatically include an external payment API or shipping service. Marking a message processed before the external call risks silently skipping an action. Calling the provider first risks repeating it after a database crash.
That calls for other safeguards: a provider idempotency key, durable outgoing work, explicit intermediate states or a compensation flow. Transactional Inbox does not make independent systems one atomic transaction.
How long should the inbox remember?
Delete deduplication records too soon and an old message replay may look new again. Keep every record forever and the inbox may grow unnecessarily. Retention has to reflect possible replay windows and the cost of repeating the business effect.
I would test not merely whether an inbox table exists, but whether the consumer behaves correctly after crashing between steps, receiving the same message concurrently and replaying older work.
A Transactional Inbox is memory of work already done. That memory must agree with the local state change, while external side effects need their own protection.