Files

3.2 KiB

outbox

Companion code for Transactional Outbox with the Spring Modulith Event Publication Registry on ankurm.com.

A single billing module demonstrating the dual-write problem, Spring Modulith's event publication registry acting as a real transactional outbox, externalizing events to a real (embedded) Kafka broker via spring-modulith-events-kafka, and replaying an incomplete publication with IncompleteEventPublications. Every number in the companion post traces to output/, in order.

Versions

Artifact Version
Spring Boot 4.1.1
Spring Modulith 2.1.1 (GA; verified against Maven Central's maven-metadata.xml)
spring-modulith-events-kafka 2.1.1
spring-kafka / spring-kafka-test 4.1.1 (managed by the Spring Boot 4.1.1 BOM; kafka-clients 4.2.1)
JDK 25 (LTS)
H2 runtime, in-memory, for the demo only

Quickstart

mvn -pl outbox -am test -Dtest=DualWriteProblemTest
mvn -pl outbox -am test -Dtest=RegistrySafetyNetTest
mvn -pl outbox -am test -Dtest=ReplayIncompletePublicationsTest

Run separately on purpose -- RegistrySafetyNetTest starts a real embedded Kafka broker (@EmbeddedKafka), which takes several seconds and is unrelated to what the other two tests are checking.

What each class is

Class Role
billing.BillingManagement The outbox-safe way to issue an invoice: one local transaction, one Invoice row, one event publication registry row
billing.NaiveBillingService The "before" picture -- a separate, uncoordinated kafka.send() call after the database write already committed. Never wired as the module's real API; only ever called directly from DualWriteProblemTest
billing.InvoiceIssued @Externalized("invoices::#{#this.invoiceId}") -- routes to the invoices Kafka topic, keyed by the invoice id
billing.FlakyAuditListener (test-only) Throws on its first invocation, succeeds after -- stands in for any downstream dependency being briefly unavailable, without needing to actually break a running broker mid-test

Captured output (output/)

File What it captures
00-dual-write-problem.txt The invoice committed, Kafka never received either message, and a real stack trace showing KafkaTemplate.send() throwing synchronously when the broker is totally unreachable -- narrower than, and a correction to, the usual "fire-and-forget send() fails silently" claim
01-registry-safety-net.txt The real production path: a real embedded Kafka broker receiving the real message, and the registry moving both listeners' rows into EVENT_PUBLICATION_ARCHIVE (completion-mode=ARCHIVE is turned on for this module -- the previous post in this series left it commented out)
02-replay-incomplete-publications.txt A listener failing once, the registry leaving its row incomplete, and IncompleteEventPublications.resubmitIncompletePublicationsOlderThan(Duration) -- a real method, verified against the compiled events-api jar -- redelivering it successfully

No docs/ chapter directory in this repository -- the intermediate and reference-depth material that would normally live there is in accordion sections inside the WordPress post itself.