Files

62 lines
3.6 KiB
Markdown

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