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.