An outbox relay moves a committed event through pending, claimed, and delivered states without folding a remote side effect into the producer transaction.
Spring outbox relay: claim, deliver, and acknowledge one event
The checked sequence
The fixture commits a shipment request and one outbox event together. A worker then claims the event with a token and lease, calls a separate consumer transaction, and acknowledges the event. The stock balance changes from 10 to 7 once. The delivered row cannot be claimed again. This sequence is invoked by a test, not a running background service.
The producer write proves one local transaction. The claim prevents a second sequential worker from taking an unexpired lease. The consumer ledger handles a repeat delivery. None of those steps makes a network publish atomic with a database commit.
State transitions have owners
PENDING is durable work waiting for a poller. DELIVERED means this model received a successful consumer result and completed a token-checked acknowledgement; it is not proof that an external broker persisted an event. A crash after consumer commit but before acknowledgement leaves PENDING work for another claim. The consumer must accept the same event ID without repeating the business mutation.
Keep the outbox row long enough for support and audit, then define retention separately. An index on status, next_attempt_at, and lease_until would matter at scale; this tiny H2 fixture does not measure a polling query plan.
Checked source
accept("EVENT-21", "SKU-17", 3, false);
claim("EVENT-21", "worker-A", 100, 50);
consume("EVENT-21");
acknowledge("EVENT-21", "worker-A", 110);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
The JUnit test calls each stage in one process with a logical clock. It has no broker, scheduler thread, crash injector, or cross-node clock agreement. Delivery semantics across a real network need a separate integration test.
Common Mistakes
- Do not mark an event delivered before checking the downstream result.
- Do not treat an outbox insert as a completed publish.
- Do not delete pending rows because a worker process restarted.
Read next
Spring transactional outbox: commit a receipt and event row together, Spring outbox claims: lease expiry and token-fenced acknowledgement, Spring consumer idempotency: reserve an event ID with the stock mutation, Spring relay failure trace: inspect persisted state at each cut point.
Continue with scheduled relay checks
Continue with Spring TaskScheduler: poll committed outbox rows in the background.
