Skip to main content

Atomic intent and lost replies

What this concept is​

The producer transaction saves business state and the intent to publish together. The outbox is that durable intent within the same local database. Sending to another system happens later and cannot be made part of this local transaction merely by placing the call between BEGIN and COMMIT.

Why it matters here​

Publish before commit can expose an event for rolled-back state. Commit business state and then attempt an unrecorded publish can lose the event after a crash. An outbox removes this particular gap by recording the pending work with the state change.

Concrete example​

For event a with amount 7, failure after the order insert or after the outbox insert but before commit leaves neither row. Failure after commit leaves both rows even when the caller sees an exception. Retrying a with amount 7 returns the existing result without adding another order; retrying a with amount 8 is a conflict.

Common confusion / misconception​

A lost response does not mean the transaction rolled back. Allocating a fresh request ID on each retry defeats the producer’s deduplication contract. An ID also needs a defined scope and immutable payload relationship; blindly accepting any repeated ID can conceal caller mistakes.

How to use it​

Own a transaction only on an idle connection. Validate and bind inputs, insert both rows, commit, then report success. Roll back before-commit errors and preserve already committed state after a simulated lost reply. Reject an active caller transaction without closing it. The checker uses BEGIN IMMEDIATE in SQLite; that is not a general proof of every database isolation behavior.

Check yourself​

Which rows exist after after_commit, and why can a retry be safe despite the caller’s uncertainty? Why can a unique order ID alone not repair an order that was committed without its outbox row?

Mini drill or application​

Draw the four boundaries: before transaction, after order, after outbox, after commit. Predict durable rows after each controlled exception, then compare with the lab. Add a different pre-existing order and show it remains unchanged.

Read this only if stuck​

Use DDIA on transactions and Transactional outbox from the pattern author. Read the atomic-intent and relay-duplicate argument; do not turn its eventual-send goal into an unconditional availability promise.