A tenant command transaction commits its receipt change, outbox event and replay record as one database unit.
Spring tenant command transaction: keep state, event and replay record together
Three rows, one commit
The local command fixture starts with the same receipt ID in two tenants. A valid tenant-east token and receipt.write scope enter a managed service. The service binds tenant-east and R-41 to a versioned UPDATE, inserts an outbox event, then saves the response version under the tenant-scoped idempotency key. The accepted request leaves the east row at version 2, one event and one replay record. The west row stays unchanged.
The service uses TransactionTemplate with a DataSourceTransactionManager. All three JDBC operations share one H2 DataSource and run on the request thread. A thrown exception after event insertion rolls back all three tables. Compare the isolated outbox test with the combined rollback test. Neither makes external message delivery part of the database transaction.
Keep identity outside client-controlled fields
The tenant argument reaches the service only after a method rule compares it with the validated JWT principal. The URL identifies requested work; it grants nothing. The SQL update includes tenant_id, receipt_id and expected version. Each predicate closes a different hole. See tenant-scoped SQL and versioned mutation.
Checked source
int changed = jdbc.update("update receipt_state set state = ?, version = version + 1 "
+ "where tenant_id = ? and receipt_id = ? and version = ?",
nextState, trustedTenant, receiptId, expectedVersion);
if (changed != 1) throw new ResponseStatusException(HttpStatus.CONFLICT);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 proof uses one process and one H2 database. It does not establish multi-worker key races, target-database lock behavior, broker delivery, crash recovery or production migration safety. A transaction holds a connection and locks until commit; keep non-database I/O outside it.
Common Mistakes
- Do not publish an event before the database transaction commits.
- Do not omit tenant_id from a write predicate.
- Do not equate a stored outbox row with delivered work.
Read next
Spring transactional outbox: commit a receipt and event row together, Spring JDBC versioned tenant update: inspect the affected row count, Spring POST idempotency keys: bind replay to tenant and command, Spring command rollback test: inspect state after an injected failure.
