Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Provides distributed transaction patterns using the Saga Pattern for Spring Boot microservices. Use when implementing distributed transactions across services, handling compensating transactions, ensuring eventual consistency, or building choreography or orchestration-based sagas with Kafka, RabbitMQ, or Axon Framework.
.claude/skills/giuseppe-trisciuoglio-spring-boot-saga-pattern/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 61% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 78% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 87% | 0% |
| case-19 | ✓→✓ | = Same ✓ | 83% | 0% |
Implements distributed transactions across microservices using the Saga Pattern. Replaces two-phase commit with a sequence of local transactions and compensating actions. Supports choreography (event-driven) and orchestration (centralized coordinator) approaches with Kafka, RabbitMQ, or Axon Framework.
Trigger phrases: distributed transactions, saga pattern, compensating transactions, microservices transaction, eventual consistency, rollback across services, orchestration pattern, choreography pattern
Map the sequence of operations and their compensating transactions:
Order → Payment → Inventory → Shipment
↓ ↓ ↓ ↓
Cancel Refund Release CancelValidation: Verify every forward step has a corresponding compensation.
| Approach | Use Case | Stack | |----------|----------|-------| | Choreography | Greenfield, few participants | Spring Cloud Stream + Kafka/RabbitMQ | | Orchestration | Complex workflows, brownfield | Axon Framework, Eventuate Tram, Camunda |
Validation: Review team expertise and system complexity before choosing.
Each service completes its local ACID transaction atomically:
java@Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; private final KafkaTemplate<String, Object> kafka; @Transactional public Order createOrder(CreateOrderCommand cmd) { Order order = orderRepository.save(new Order(cmd.orderId(), cmd.items())); kafka.send("order.created", new OrderCreatedEvent(order.getId(), order.getItems())); return order; } }
Validation: Test that local transaction commits before event is published.
Every forward operation requires an idempotent compensation:
java@Service @RequiredArgsConstructor public class PaymentService { private final PaymentRepository paymentRepository; private final KafkaTemplate<String, Object> kafka; public void processPayment(PaymentRequest request) { Payment payment = paymentRepository.save(new Payment(request.orderId(), request.amount())); kafka.send("payment.processed", new PaymentProcessedEvent(payment.getId(), request.orderId())); } @Transactional public void refundPayment(String paymentId) { paymentRepository.findById(paymentId) .ifPresent(p -> { p.setStatus(REFUNDED); paymentRepository.save(p); kafka.send("payment.refunded", new PaymentRefundedEvent(paymentId)); }); } }
Validation: Confirm compensation can execute safely multiple times (idempotency).
Configure Kafka with idempotent consumers:
java@Configuration @EnableKafka public class KafkaConfig { @Bean public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory( ConsumerFactory<String, Object> consumerFactory) { ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory); factory.setCommonErrorHandler(new DefaultErrorHandler()); return factory; } }
Validation: Enable transactional ID and verify exactly-once semantics.
java@Service @RequiredArgsConstructor public class OrderSagaOrchestrator { private final KafkaTemplate<String, Object> kafka; private final SagaStateRepository sagaStateRepo; public void startSaga(OrderRequest request) { String sagaId = UUID.randomUUID().toString(); sagaStateRepo.save(new SagaState(sagaId, STARTED, LocalDateTime.now())); kafka.send("saga.order.start", new StartOrderSagaCommand(sagaId, request)); } @KafkaListener(topics = "payment.failed") public void handlePaymentFailed(PaymentFailedEvent event) { kafka.send("order.compensate", new CompensateOrderCommand(event.getSagaId())); kafka.send("inventory.compensate", new ReleaseInventoryCommand(event.getSagaId())); sagaStateRepo.updateStatus(event.getSagaId(), FAILED); } }
Validation: Verify saga state persists before sending commands. Check compensation triggers on each failure path.
java@Service public class OrderEventHandler { private final OrderService orderService; private final KafkaTemplate<String, Object> kafka; @KafkaListener(topics = "payment.processed", groupId = "order-service") public void onPaymentProcessed(PaymentProcessedEvent event) { try { InventoryReservedEvent result = orderService.reserveInventory(event.toInventoryRequest()); kafka.send("inventory.reserved", result); } catch (InsufficientInventoryException e) { kafka.send("inventory.insufficient", new InsufficientInventoryEvent(event.getOrderId(), event.getPaymentId())); } } }
Validation: Test that each event handler correctly triggers the next step or compensation.
java@Configuration public class SagaMetricsConfig { @Bean public MeterRegistry meterRegistry() { return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); } }
Track: saga execution duration, compensation count, failure rate, stuck sagas.
Validation: Set up alerts for sagas exceeding expected duration.
Design:
Error Handling:
Monitoring:
java// Application.java @SpringBootApplication @EnableKafka @EnableKafkaListeners public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } // Event Classes (immutable) public record OrderCreatedEvent(String orderId, List<OrderItem> items) {} public record PaymentProcessedEvent(String paymentId, String orderId) {} public record InventoryReservedEvent(String reservationId, String orderId) {} public record PaymentFailedEvent(String orderId, String reason) {} public record InsufficientInventoryEvent(String orderId, String paymentId) {} // OrderService with compensation @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; private final KafkaTemplate<String, Object> kafka; @KafkaListener(topics = "payment.failed", groupId = "order-service") public void handleCompensation(PaymentFailedEvent event) { orderRepository.findByOrderId(event.orderId()) .ifPresent(order -> { order.setStatus(CANCELLED); orderRepository.save(order); }); } }
java// Command @Aggregate public class OrderAggregate { @AggregateIdentifier private String orderId; @CommandHandler public OrderAggregate(CreateOrderCommand cmd) { apply(new OrderCreatedEvent(cmd.orderId(), cmd.items())); } @EventSourcingHandler public void on(OrderCreatedEvent event) { this.orderId = event.orderId(); } @CommandHandler public void handle(CancelOrderCommand cmd) { apply(new OrderCancelledEvent(cmd.orderId(), cmd.reason())); } }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | pass→pass | 15,073 | 14,081 | -7% | 1 | 1 | 0% | 2,551 | 4,680 | +83% | 0 | 0 | — |
case-01 | fail→pass | 18,186 | 11,979 | -34% | 1 | 1 | 0% | 2,679 | 4,319 | +61% | 0 | 0 | — |
case-02 | fail→pass | 15,785 | 12,393 | -21% | 1 | 1 | 0% | 2,455 | 4,363 | +78% | 0 | 0 | — |
case-03 | pass→pass | 12,794 | 11,265 | -12% | 1 | 1 | 0% | 2,173 | 4,002 | +84% | 0 | 0 | — |
case-04 | pass→pass | 13,595 | 11,527 | -15% | 1 | 1 | 0% | 2,422 | 4,043 | +67% | 0 | 0 | — |
case-05 | pass→pass | 13,287 | 11,203 | -16% | 1 | 1 | 0% | 2,207 | 3,812 | +73% | 0 | 0 | — |
case-06 | pass→pass | 11,204 | 9,701 | -13% | 1 | 1 | 0% | 1,678 | 3,656 | +118% | 0 | 0 | — |
case-07 | pass→pass | 13,266 | 10,262 | -23% | 1 | 1 | 0% | 2,084 | 3,783 | +82% | 0 | 0 | — |
case-08 | fail→fail | 15,379 | 10,689 | -30% | 1 | 1 | 0% | 2,553 | 3,844 | +51% | 0 | 0 | — |
case-09 | pass→pass | 11,284 | 9,570 | -15% | 1 | 1 | 0% | 1,758 | 3,901 | +122% | 0 | 0 | — |
case-10 | pass→pass | 12,711 | 5,800 | -54% | 1 | 1 | 0% | 2,318 | 3,101 | +34% | 0 | 0 | — |
case-11 | pass→pass | 14,242 | 8,008 | -44% | 1 | 1 | 0% | 2,429 | 3,510 | +45% | 0 | 0 | — |
case-12 | pass→pass | 14,447 | 11,699 | -19% | 1 | 1 | 0% | 2,600 | 4,382 | +69% | 0 | 0 | — |
case-18 | pass→pass | 14,468 | 8,489 | -41% | 1 | 1 | 0% | 2,575 | 3,762 | +46% | 0 | 0 | — |
case-13 | fail→pass | 11,197 | 2,915 | -74% | 1 | 1 | 0% | 1,676 | 2,600 | +55% | 0 | 0 | — |
case-14 | pass→pass | 11,919 | 8,621 | -28% | 1 | 1 | 0% | 2,061 | 3,722 | +81% | 0 | 0 | — |
case-15 | pass→pass | 11,799 | 9,741 | -17% | 1 | 1 | 0% | 1,709 | 3,784 | +121% | 0 | 0 | — |
case-16 | pass→pass | 7,098 | 6,534 | -8% | 1 | 1 | 0% | 1,421 | 3,320 | +134% | 0 | 0 | — |
case-17 | pass→pass | 14,953 | 10,563 | -29% | 1 | 1 | 0% | 2,984 | 4,073 | +36% | 0 | 0 | — |
case-20 | pass→pass | 21,898 | 7,294 | -67% | 1 | 1 | 0% | 1,668 | 3,319 | +99% | 0 | 0 | — |
case-21 | fail→pass | 18,955 | 23,134 | +22% | 1 | 1 | 0% | 3,586 | 6,693 | +87% | 0 | 0 | — |
case-22 | pass→pass | 18,669 | 14,301 | -23% | 1 | 1 | 0% | 3,593 | 5,058 | +41% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.