Contents
- Dependency & Coordinator
- Basic Saga Route
- Compensation Routes
- Saga Options & Propagation
- Saga Timeout
- Completion & Compensation Callbacks
- Full Order Saga Example
- MicroProfile LRA Integration
Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-saga-starter</artifactId>
</dependency>
@Configuration
public class SagaConfig {
// NaiveLRACoordinator — in-memory, suitable for single-node / dev
// Use camel-lra with a real coordinator (Narayana) for production multi-node
@Bean
public CamelSagaService sagaService() {
InMemorySagaService service = new InMemorySagaService();
return service;
}
}
The .saga() DSL marks the beginning of a saga. Each .compensation("uri") call registers the route to invoke if the saga is cancelled.
@Component
public class OrderSagaRoute extends RouteBuilder {
@Override
public void configure() {
// Main saga — each step declares its compensation
from("direct:placeOrder")
.saga()
.timeout(Duration.ofMinutes(5)) // cancel saga if not complete in 5min
.log("Starting order saga for ${body.orderId}")
// Step 1 — Reserve inventory
.to("direct:reserveInventory")
// Step 2 — Charge payment
.to("direct:chargePayment")
// Step 3 — Create shipment
.to("direct:createShipment")
.log("Order saga completed successfully");
}
}
Each participant route that modifies state must declare a compensation route. Camel invokes it automatically when the saga is cancelled due to a downstream failure.
@Component
public class InventoryRoute extends RouteBuilder {
@Override
public void configure() {
// Step — reserve inventory, declare its compensation
from("direct:reserveInventory")
.saga()
.propagation(SagaPropagation.MANDATORY) // must be part of an existing saga
.compensation("direct:releaseInventory") // called if saga fails
.option("reservationId", header("reservationId")) // pass data to compensation
.bean(inventoryService, "reserve")
.log("Inventory reserved: ${header.reservationId}");
// Compensation — release the reservation
from("direct:releaseInventory")
.log("Compensating: releasing reservation ${header.reservationId}")
.bean(inventoryService, "release");
// Payment step with its compensation
from("direct:chargePayment")
.saga()
.propagation(SagaPropagation.MANDATORY)
.compensation("direct:refundPayment")
.option("transactionId", header("transactionId"))
.bean(paymentService, "charge")
.log("Payment charged: ${header.transactionId}");
from("direct:refundPayment")
.log("Compensating: refunding transaction ${header.transactionId}")
.bean(paymentService, "refund");
}
}
| Propagation | Behaviour |
| REQUIRED (default) | Join existing saga or create new one |
| REQUIRES_NEW | Always create a new saga, suspend existing |
| MANDATORY | Must join an existing saga; throw if none active |
| SUPPORTS | Join existing saga if present; run without saga otherwise |
| NOT_SUPPORTED | Run outside any saga |
| NEVER | Throw if a saga is active |
// Pass saga-scoped data via .option() — available in compensation routes
from("direct:chargePayment")
.saga()
.propagation(SagaPropagation.MANDATORY)
.compensation("direct:refundPayment")
// Store headers for compensation to use (saga-scoped, not just exchange-scoped)
.option("amount", header("amount"))
.option("accountId", header("accountId"))
.to("direct:paymentGateway");
The class below shows the implementation. Key points are highlighted in the inline comments.
from("direct:placeOrder")
.saga()
.timeout(Duration.ofMinutes(2)) // saga auto-cancels after 2 minutes
.to("direct:reserveInventory")
.to("direct:chargePayment")
.to("direct:createShipment");
// On timeout, Camel triggers compensation for all completed steps automatically
Saga timeout is measured from the point .saga() is entered. If any step is slow (e.g., waiting on an external service), the timeout fires and triggers compensation for all steps that already completed — preventing partial saga completion from being left in place indefinitely.
The class below shows the implementation. Key points are highlighted in the inline comments.
from("direct:placeOrder")
.saga()
.completion("direct:onOrderComplete") // called on successful saga finish
.compensation("direct:onOrderCancelled") // called on saga failure/timeout
.to("direct:reserveInventory")
.to("direct:chargePayment")
.to("direct:createShipment");
// Saga-level completion handler — send customer notification
from("direct:onOrderComplete")
.log("Saga completed — notifying customer")
.to("direct:sendConfirmationEmail");
// Saga-level compensation handler — log and alert
from("direct:onOrderCancelled")
.log(LoggingLevel.ERROR, "Saga cancelled — order ${header.orderId} rolled back")
.to("direct:sendCancellationEmail");
The class below shows the implementation. Key points are highlighted in the inline comments.
@Component
public class FullOrderSaga extends RouteBuilder {
@Override
public void configure() {
onException(Exception.class)
.handled(true)
.log(LoggingLevel.ERROR, "Saga step failed: ${exception.message}")
.to("direct:sagaFailure");
// Orchestrator
from("direct:placeOrder")
.saga().timeout(Duration.ofMinutes(5))
.setHeader("orderId", simple("${body.orderId}"))
.to("direct:reserveInventory") // step 1
.to("direct:chargePayment") // step 2
.to("direct:scheduleDelivery") // step 3
.log("Order ${header.orderId} saga complete");
// Inventory
from("direct:reserveInventory")
.saga().propagation(SagaPropagation.MANDATORY)
.compensation("direct:releaseInventory")
.bean("inventoryService", "reserve");
from("direct:releaseInventory")
.bean("inventoryService", "release");
// Payment
from("direct:chargePayment")
.saga().propagation(SagaPropagation.MANDATORY)
.compensation("direct:refundPayment")
.option("chargeId", header("chargeId"))
.bean("paymentService", "charge");
from("direct:refundPayment")
.bean("paymentService", "refund");
// Delivery
from("direct:scheduleDelivery")
.saga().propagation(SagaPropagation.MANDATORY)
.compensation("direct:cancelDelivery")
.bean("deliveryService", "schedule");
from("direct:cancelDelivery")
.bean("deliveryService", "cancel");
}
}
For production multi-node deployments, replace the in-memory coordinator with a real LRA coordinator (Narayana) via camel-lra.
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-lra-starter</artifactId>
</dependency>
# application.yml
camel:
lra:
coordinator-url: http://lra-coordinator:8080 # Narayana LRA coordinator
local-participant-url: http://payment-service:8080 # this service's callback URL
local-participant-context-path: /lra-participant