# saga Companion code for *Saga Pattern in Spring Boot: Orchestration vs Choreography with Kafka* on [ankurm.com](https://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.