Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring outbox to Kafka: name the crash window between send and relay acknowledgement

Last updated: 1 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

A database outbox protects the command write, while an uncertain Kafka send requires replay with a stable event ID.

Download Spring source kit

One database commit; two delivery outcomes

The existing receipt command stores business state and an outbox row in one database transaction. A separate relay claims that row and sends a Kafka record. If the send fails before broker acceptance, the relay can retry. If Kafka accepts it and the relay crashes before marking the row delivered, a retry may publish it again. No ordering of these two independent commits erases both crash windows. A fenced relay acknowledgement limits stale workers; the event ledger limits repeated stock mutation.

The fixture's producer future is observed. It is not connected to the outbox H2 tests as an end-to-end process, and there is no Kafka broker in the kit. A production relay still needs serialization, headers or schema versioning, bounded retry, lease renewal, metrics and a recovery queue for non-retryable records. The relay project describes those operational states.

Preserve event identity through every attempt

The outbox row should carry a durable event ID and payload version. Every retry sends that same identity. If an editor or a worker recomputes payload from mutable current database state, the repeated event may no longer represent the command that originally committed. Store enough data to reproduce the intended event or define an explicit projection contract.

Code boundary

Java
// The checked gateway exposes completion; the relay must not mark a row delivered
// until it has decided what this completion means for its retry policy.
CompletableFuture<SendResult<String, StockReservation>> sent =
    operations.send("stock-reservations", reservation.sku(), reservation);

Verification boundary

The Java 21 / Spring Boot 4 source kit checks failedSendRemainsFailedForCallerToHandle plus the source kit's existing H2 outbox and consumer-ledger tests; a combined outbox-to-Kafka relay is not present.

Cost and limits

The outbox adds a durable row, claim query, retention work and at least one extra delivery step. It trades a single-request send for eventual delivery with observable lag; broker acceptance and database acknowledgement still need a crash-recovery integration test.

Common Mistakes

  • Do not mark an outbox row delivered before examining the send completion.
  • Do not claim a database commit and broker send are one atomic transaction.
  • Do not assign a new event ID to every relay attempt.

Read next

Spring transactional outbox: commit a receipt and event row together, Outbox acknowledgements: require the current lease token, Spring project: turn the local outbox model into a measured relay, Spring Kafka producer future: observe broker send completion separately from command success, Spring Kafka duplicate delivery: let a unique event ID decide the second debit.

GraphQL continuation

Continue with Spring GraphQL mutation versions: reject a stale stock reservation.

spring
spring-boot
kafka
kafka-outbox-handoff
Storage details