Skip to content

Your outbox relay published twice. Is the outbox design broken?

1 min read · main part only; folded sections are optional

After this you can answer: Your outbox relay published twice. Is the outbox design broken?

The outbox promises that a committed booking retains an event to deliver. It does not promise one network publish. A lost SNS response can be ambiguous too: the relay sees a failure even if SNS accepted the request.

The relay uses stable identifiers from the stored payload and validates that they match its outbox row. Consumers should deduplicate the business operation. The email worker uses booking.confirmed plus the booking ID, rather than depending on the transport message ID. The ticket worker derives one ticket ID per booking seat.

For production, I would monitor publish failures, pending-row age and consumer completion separately. A growing outbox might mean a policy denial, topic misconfiguration or a database problem, not just harmless retries. Delivery is only useful when the downstream effect eventually happens, and the email consumer still has documented duplicate windows around its external send.

Why keep row locks while calling SNS?
They stop another relay task from taking those rows during normal delivery. The cost is a connection and open transaction during network work; bounded calls and small batches limit that cost.
What if marking the row sent fails?
The transaction rolls back. The already accepted SNS event cannot be rolled back, so the row remains eligible for another publish and the consumer may see a duplicate.
Can a FIFO queue solve the whole problem?
It can add transport deduplication within its contract, but it does not atomically commit the database, email provider and queue. Business-level idempotency and explicit recovery are still needed.

Read Outbox, then inspect the outbox piece →.

apps/api/src/queries/outbox.ts → deliverBatch

Rehearse the consumer limit with the duplicate-email case.

Read this book online: https://book.theversehub.in/