The producer transaction and consumer transaction are separate commits with a delivery gap between them.
Spring outbox transaction boundaries: where atomicity ends
Producer rollback
The producer fixture inserts a shipment request and an event row in one TransactionTemplate callback. An injected runtime failure after both inserts leaves neither row. A successful call leaves one of each. That is local database atomicity, not a guarantee that another service received the event. The earlier outbox lesson covers the same principle from the command side.
The relay later claims the row, calls the consumer, and acknowledges it. Those steps must not be wrapped in one long transaction across a remote wait. The consumer commits its own event marker and stock mutation together. A crash between that commit and relay acknowledgement means delivery may repeat; the marker is the recovery boundary.
Handle failure evidence
An exception from a JDBC statement should leave an observable rolled-back row count in the test. A timeout from a remote call is weaker evidence: it may mean the response was lost after commit. The local kit tests producer rollback and consumer rollback with H2; it does not simulate a process kill, network partition, broker offset or two databases.
Spring rollback behavior depends on the transaction manager and exception type. The fixture uses unchecked exceptions with a JDBC transaction manager. If an application uses checked exceptions or catches a failure inside a callback, specify the rollback rule and assert persisted state.
Checked source
transactions.executeWithoutResult(status -> {
jdbc.update("insert into shipment_request values (?)", requestId);
jdbc.update("insert into delivery_outbox values (?, ?, ?, 'PENDING', null, null, 0, 0)", eventId, sku, units);
});Verification boundary
OutboxRelayFlowTest has seven local tests in the downloadable Spring source kit. The excerpt is shortened; the kit contains the complete test.
Costs and limits
One H2 datasource and sequential JUnit calls are checked. There is no distributed transaction, durable broker, real process restart, or target-database isolation test.
Common Mistakes
- Do not publish remotely inside a local transaction and assume rollback will retract it.
- Do not swallow a database failure and assume rollback still happens.
- Do not mark the producer commit as delivery completion.
Read next
Spring transactional outbox: commit a receipt and event row together, Spring consumer idempotency: reserve an event ID with the stock mutation, Spring transaction rollback: checked failures commit unless a rule says otherwise, Spring relay failure trace: inspect persisted state at each cut point.
