Files

55 lines
3.2 KiB
Markdown

# 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.