3.6 KiB
saga
Companion code for Saga Pattern in Spring Boot: Orchestration vs Choreography with Kafka on ankurm.com.
The same order/payment/inventory saga, implemented twice against a real (embedded) Kafka broker: once as choreography (every participant reacts to the previous step's event and decides for itself what happens next) and once as orchestration (one class issues commands and makes every decision; participants only ever answer a command). Both versions are driven through the same failure -- insufficient inventory -- so the compensating transaction that undoes the reserved payment can be compared side by side. A fourth test demonstrates the idempotent-consumer problem concretely: the same command, redelivered, reserving stock only once.
Versions
| Artifact | Version |
|---|---|
| Spring Boot | 4.1.1 (GA; verified against Maven Central's maven-metadata.xml -- 4.2.0-M2 is the newest entry there but is a milestone, not GA) |
| Spring Kafka | 4.1.1, managed by the Spring Boot BOM |
| JDK | 25 (LTS) |
| H2 | runtime, in-memory, for the demo only |
Quickstart
mvn -pl saga test -Dtest=ChoreographyHappyPathTest
mvn -pl saga test -Dtest=ChoreographyCompensationTest
mvn -pl saga test -Dtest=OrchestrationSagaTest
mvn -pl saga test -Dtest=IdempotentConsumerTest
Each test class starts its own embedded Kafka broker and its own isolated in-memory H2
database (a random schema name per test, via @DynamicPropertySource), so running them
individually or together (mvn -pl saga test) gives the same results.
What each class is
| Class | Role |
|---|---|
choreography.ChoreographySagaStarter |
Creates the order, publishes OrderCreated, and does nothing else -- the only choreography class that "starts" anything |
choreography.PaymentChoreographyListener |
Reserves payment on OrderCreated; refunds it on InventoryRejected (the compensating transaction) |
choreography.InventoryChoreographyListener |
The saga's one failure trigger: reacts to PaymentReserved, decides stock is or isn't available, publishes accordingly |
choreography.OrderChoreographyListener |
Confirms or cancels the order; never decides anything, only reacts |
orchestration.OrderSagaOrchestrator |
Every saga decision lives here: what to do after each reply, including issuing the compensating RefundPaymentCommand |
orchestration.PaymentOrchestrationHandler / InventoryOrchestrationHandler |
Carry out exactly one command each and reply; never decide what happens next |
idempotency.IdempotencyGuard |
claim(commandId) -- true the first time a command id is seen, false on every redelivery, backed by a real UNIQUE constraint |
inventory.InventoryService |
In-memory stock only -- this module is about saga coordination, not inventory persistence |
Captured output (output/)
| File | What it captures |
|---|---|
00-choreography-happy-path.txt |
Enough stock: payment reserved, inventory reserved, order confirmed, with no single class aware of the whole sequence |
01-choreography-compensation.txt |
Not enough stock: inventory rejects, payment refunds itself in response, order ends cancelled |
02-orchestration-saga.txt |
Both scenarios again, driven by OrderSagaOrchestrator instead -- same outcomes, one class making every decision |
03-idempotent-consumer-duplicate.txt |
The same ReserveInventoryCommand, same commandId, delivered twice: one processed-command row, one stock reservation, one reply |
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.