Camel Saga Pattern: Long-Running Transaction Compensation
A saga breaks a long-running business transaction into a sequence of local steps, each with a compensating action that undoes its effect if a later step fails. Apache Camel provides first-class saga support via the camel-saga component: you declare each step with its compensation route, and Camel triggers compensation automatically on failure. This avoids distributed locks while still achieving eventual consistency.
Contents
Dependency & Coordinator
Add the following to your pom.xmlbuild.gradle
<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;
}
}
Basic Saga Route
The .saga().compensation("uri")
@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");
}
}
Compensation Routes
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");
}
}
Saga Options & Propagation
| Propagation | Behaviour |
|---|---|
REQUIRED | 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");
Saga Timeout
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()Completion & Compensation Callbacks
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");
Full Order Saga Example
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");
}
}
MicroProfile LRA Integration
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