Transactional inbox
Handle duplicate messages using inbox identity, an atomic database boundary and startup validation.
A transactional inbox prevents an event that has already completed for the same handler from running its business code again. Lily completes the inbox record, eligible database writes and outbox insertions in one database transaction. The RabbitMQ ACK follows that commit.
Enable all four parts together#
- Select the appropriate
consumer-transactional-inbox-*facade feature for PostgreSQL or MongoDB. - Add
delivery_guarantee = "transactional_inbox"to the handler. - Set an explicit backend, and a cell when using factory mode, in the physical queue’s
transactional_inboxsection. - Apply the Lily component migration to that database before starting Consumer.
#[queue("orders.created", version = 1, content = "json", delivery_guarantee = "transactional_inbox")]This line is a handler attribute inside an implementation marked with #[queue_service], rather than a complete application. Follow the PostgreSQL or MongoDB page for complete code and its manifest. Do not add a comma after the last #[queue] argument.
Inbox identity and retention#
The deduplication key combines the generated handler identity with the canonical event ID. Handler identity is derived from the module path, service type and method name. Schema version and content kind are not separate inbox key fields. Renaming a service, moving it to another module or changing its crate path can create a new deduplication namespace.
If a completed inbox record exists, the business pipeline is skipped and the delivery takes the successful settlement path. After that record is cleaned up according to inbox_retention_secs, the same old event can be processed again. Design replay policy, handler identity changes during deployment and retention together.
Which operations are covered?#
| Inside the boundary | Outside the boundary |
|---|---|
| Inbox claim and completion | RabbitMQ ACK |
| Writes to the same database through the supplied transaction context | Writes through another pool, cell or session |
| Outbox rows inserted in the same transaction | The relay’s broker publication |
| Database changes rolled back before commit | HTTP, email, file and Redis side effects |
This contract is neither end-to-end exactly-once nor a two-phase commit between the broker and database. A commit can succeed while its ACK fails; a later delivery is recognized through the inbox. A relay publication can succeed while marking it delivered fails; downstream consumers can see the same outbox event ID again.
Startup validation and backend differences#
Startup fails if the feature, handler metadata and physical queue binding disagree. A stale binding on a queue with no transactional handler is also rejected. The backend is never inferred and there is no silent fallback to at-least-once. Different version/content handlers on one physical queue can use both guarantee modes; its transactional handlers share one storage binding.
PostgreSQL reports a concurrent duplicate as a retryable error and uses the normal retry/DLQ policy. MongoDB lease contention can return the original delivery to its queue; certain transient transaction errors replay the entire pipeline in a fresh scope. Do not assume that both backends retry identically. The backend guides and limits explain which budget each case uses.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.
- lilyrs/Cargo.toml
- framework/lily_consumer/src/plan.rs
- integrations/lily_queue/src/transactional.rs
- integrations/lily_queue/src/queue_service.rs
- integrations/lily_queue/src/transactional_postgresql/storage.rs
- integrations/lily_queue/src/transactional_mongodb/storage.rs
- integrations/lily_queue_derive/src/utils.rs