A receipt outbox row records work to deliver after its business transaction commits.
Spring receipt outbox command: record intent without claiming delivery
Commit intent with the receipt
The checked write inserts the outbox row after the versioned state update and before the replay record. TransactionTemplate commits the three operations together. A forced failure after the outbox insert rolls back both the event and the receipt update. That is the useful guarantee: no committed event for an uncommitted state in this one database transaction.
The event ID includes tenant, receipt and request key in the local fixture; it is stable across a retry of the same command. The lease lesson covers a later worker claim, and the consumer lesson covers duplicate handling. Neither is wired into this web context. The outbox row starts as a database fact, not a message delivered to a broker.
Define the event contract
A real event needs a versioned schema, tenant-safe identifiers, ordering rules and a retention policy. Decide whether one receipt requires ordered delivery and how a consumer recognizes old or duplicate versions. Do not place an HTTP call inside the transaction to simulate atomic publication. That only holds a database connection while another system can fail independently.
Checked source
transaction.execute(status -> {
updateReceipt(trustedTenant, receiptId, expectedVersion, nextState);
jdbc.update("insert into receipt_outbox values (?, ?, ?, ?)",
eventId, trustedTenant, receiptId, nextState);
saveReplay(trustedTenant, requestKey, responseVersion);
return responseVersion;
});Verification boundary
TenantReceiptCommandFlowTest.authorizedWriteCommitsReceiptOutboxAndReplayRecord and TenantReceiptCommandFlowTest.failureAfterOutboxInsertRollsBackAllThreeTables in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete test.
Costs and limits
The test has no relay, broker, external consumer, crash between claim and acknowledgement, or concurrent first command. Row storage and later polling add write and read load; capacity planning belongs to the target database and worker workload.
Common Mistakes
- Do not call an inserted outbox row a delivered event.
- Do not do remote I/O inside the database transaction.
- Do not discard tenant identity from the event contract.
Read next
Spring tenant command transaction: keep state, event and replay record together, Spring transactional outbox: commit a receipt and event row together, Spring outbox claims: allow recovery after a worker lease expires, Spring consumer deduplication: commit the event ID with the mutation.
Continue with checked relay behavior
Continue with Spring outbox relay: claim, deliver, and acknowledge one event.
