Files

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.