# outbox Companion code for *[Transactional Outbox with the Spring Modulith Event Publication Registry](https://ankurm.com)* on [ankurm.com](https://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.