A transactional outbox stores the business change and a pending delivery record in one database transaction.
Spring transactional outbox: commit a receipt and event row together
One commit boundary
The checked H2 fixture inserts receipt R-41 and outbox event E-41 through one Spring TransactionTemplate. A failure before commit leaves neither row. A successful retry leaves one of each. The event row is a promise to attempt delivery later, not evidence that a broker has received anything.
A direct broker call inside the transaction cannot be rolled back with the database. Sending only after commit avoids that ordering problem but can still lose a message if the process exits before sending. The stored event closes that particular gap when a worker later scans it. After-commit listeners alone do not make delivery durable.
The database boundary still needs a schema
Give the event its own stable ID, a state, and enough payload or a reference to reconstruct the delivery. Insert both rows with the same DataSource and transaction manager. A second database, external queue or independent transaction breaks the atomic claim. Claiming pending rows is the next boundary, not part of this commit.
Checked source
transaction.executeWithoutResult(status -> {
jdbc.update("insert into receipt values (?, ?)", "R-41", "accepted");
jdbc.update("insert into outbox values (?, ?, ?)", "E-41", "R-41", "pending");
});Verification boundary
OutboxWriteContractTest.receiptAndOutboxRowCommitOrRollBackTogether runs in the downloadable Spring source kit. The excerpt leaves out surrounding setup and imports; the kit contains the complete test.
Costs and limits
Two indexed inserts are O(log n) in the usual B-tree model, plus commit and log I/O; that is not a latency promise. The local H2 test proves rollback and commit together for one transaction manager. It does not test broker delivery, crash recovery, production isolation or schema migration.
Common Mistakes
- Do not call an after-commit callback a durable queue.
- Do not write the business row and event row through unrelated transactions.
- Do not mark an event delivered when only the database commit succeeded.
Read next
Spring TransactionTemplate: roll back a failed multi-row change, Spring transaction events: run a listener after commit without claiming durability, Spring transaction propagation: joined rollback and independent commit.
Continue with checked worker recovery
Continue with Spring outbox claims: allow recovery after a worker lease expires, Spring consumer deduplication: commit the event ID with the mutation.
Continue with checked tenant commands
Continue with Spring receipt outbox command: record intent without claiming delivery.
Continue with checked relay behavior
Continue with Spring outbox transaction boundaries: where atomicity ends.
