Contents

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"); } }
PropagationBehaviour
REQUIRED (default)Join existing saga or create new one
REQUIRES_NEWAlways create a new saga, suspend existing
MANDATORYMust join an existing saga; throw if none active
SUPPORTSJoin existing saga if present; run without saga otherwise
NOT_SUPPORTEDRun outside any saga
NEVERThrow 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