Add the saga module: Saga Pattern in Spring Boot, orchestration vs choreography with Kafka
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
$ mvn -pl saga test -Dtest=ChoreographyHappyPathTest
|
||||
(real embedded Kafka broker via @EmbeddedKafka; ChoreographySagaStarter.placeOrder() publishes
|
||||
OrderCreated, and every step after that is a listener reacting to the previous one's event --
|
||||
no single class decides the whole saga)
|
||||
|
||||
Order 1 status: CONFIRMED
|
||||
Stock remaining for GADGET-1: 8
|
||||
Payment record status: RESERVED
|
||||
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
|
||||
Stock started at 10, the order asked for 2, and 8 is what's left -- the reservation really ran.
|
||||
Five separate listeners fired in sequence to get here: PaymentChoreographyListener reacted to
|
||||
OrderCreated and reserved payment; InventoryChoreographyListener reacted to PaymentReserved,
|
||||
found enough stock, and published InventoryReserved; OrderChoreographyListener reacted to that
|
||||
and confirmed the order. None of those three classes knows about the other two's existence --
|
||||
each one only knows "when I see event X, I do Y, then publish Z".
|
||||
@@ -0,0 +1,15 @@
|
||||
$ mvn -pl saga test -Dtest=ChoreographyCompensationTest
|
||||
(same saga, stock deliberately too low: 1 unit on hand, order asks for 5)
|
||||
|
||||
Order 1 status: CANCELLED
|
||||
Stock remaining for GADGET-1 (untouched by the rejected reservation): 1
|
||||
Payment record status: REFUNDED
|
||||
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
|
||||
InventoryChoreographyListener's tryReserve() refused before touching the stock map at all --
|
||||
the 1 unit is exactly where it started. That rejection published InventoryRejected, which
|
||||
PaymentChoreographyListener is also subscribed to; it refunded the payment it had reserved
|
||||
earlier and published PaymentRefunded, which OrderChoreographyListener used to cancel the
|
||||
order. The compensating transaction (the refund) is ordinary code in a class that already
|
||||
existed for an unrelated reason -- nothing new coordinates it.
|
||||
@@ -0,0 +1,20 @@
|
||||
$ mvn -pl saga test -Dtest=OrchestrationSagaTest
|
||||
(the same two scenarios -- enough stock, and not enough stock -- run through
|
||||
OrderSagaOrchestrator instead of through independent listeners)
|
||||
|
||||
[happy path] Order 1 status: CONFIRMED
|
||||
[happy path] Stock remaining for GADGET-ORCH-OK: 8
|
||||
[happy path] Payment record status: RESERVED
|
||||
[compensation] Order 2 status: CANCELLED
|
||||
[compensation] Stock remaining for GADGET-ORCH-FAIL (untouched): 1
|
||||
[compensation] Payment record status: REFUNDED
|
||||
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||
|
||||
Same outcomes as the choreography transcripts, reached a structurally different way. Here,
|
||||
PaymentOrchestrationHandler and InventoryOrchestrationHandler never decide what happens next --
|
||||
they carry out exactly one command each and reply. OrderSagaOrchestrator is the only class that
|
||||
reads a reply and decides what command to send next, including the decision, in the second
|
||||
test, to send RefundPaymentCommand once InventoryReply says no. Compare this transcript's two
|
||||
tests with the two choreography transcripts above: identical business outcomes, from listener
|
||||
classes that know nothing about the saga as a whole versus one class that knows all of it.
|
||||
@@ -0,0 +1,16 @@
|
||||
$ mvn -pl saga test -Dtest=IdempotentConsumerTest
|
||||
(the exact same ReserveInventoryCommand -- same commandId -- published twice by hand, to
|
||||
simulate a redelivery; stock starts at 10, the command asks for 3)
|
||||
|
||||
ProcessedCommand rows for commandId f07ec35c-853c-4405-a024-b62d304dfabb: 1
|
||||
Stock remaining for GADGET-DUP after two deliveries of the same command: 7
|
||||
InventoryReply messages actually published: 1
|
||||
reply: InventoryReply[commandId=f07ec35c-853c-4405-a024-b62d304dfabb, orderId=999, success=true, reason=null]
|
||||
|
||||
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
|
||||
|
||||
Two deliveries, one row in processed_commands, one actual reservation: 10 - 3 = 7, not 4. The
|
||||
second delivery hit IdempotencyGuard.claim(), got false back because the UNIQUE constraint on
|
||||
commandId rejected the second insert, and returned from the listener before calling
|
||||
InventoryService.tryReserve() at all -- it never touched stock, and it never published a second
|
||||
InventoryReply. Exactly one reply reached the topic for two deliveries of the same command.
|
||||
Reference in New Issue
Block a user