SagaContext
The immutable object that travels between saga steps.
Concept
SagaContext is a pattern, not an interface. You define your own context as a record or immutable class.
Implementation with Record
public record OrderContext(
// Input data (set at creation)
String orderId,
String customerId,
List<OrderItem> items,
BigDecimal totalAmount,
// Step outputs (populated during execution)
String stockReservationId,
String paymentIntentId,
String shippingTrackingId
) {
/**
* Creates an initial context with only input data.
*/
public static OrderContext create(Order order) {
return new OrderContext(
order.getId(),
order.getCustomerId(),
List.copyOf(order.getItems()),
order.getTotal(),
null, null, null // Outputs start null
);
}
// --- Immutable builders ---
public OrderContext withStockReservationId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
id, paymentIntentId, shippingTrackingId
);
}
public OrderContext withPaymentIntentId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
stockReservationId, id, shippingTrackingId
);
}
public OrderContext withShippingTrackingId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
stockReservationId, paymentIntentId, id
);
}
}
Implementation with Lombok
@Value
@With
@Builder
public class OrderContext {
String orderId;
String customerId;
List<OrderItem> items;
BigDecimal totalAmount;
String stockReservationId;
String paymentIntentId;
String shippingTrackingId;
public static OrderContext create(Order order) {
return OrderContext.builder()
.orderId(order.getId())
.customerId(order.getCustomerId())
.items(List.copyOf(order.getItems()))
.totalAmount(order.getTotal())
.build();
}
}
// Usage
ctx.withStockReservationId("RES-123");
Requirements
Must be
- Serializable to JSON — Sagaweaw persists as JSONB
- Immutable — Each step returns a new instance
- No circular references — JSON doesn't support them
Avoid
- Mutable collections (ArrayList, HashMap)
- References to JPA entities
- Complex non-serializable objects
Serialization
The context is serialized automatically:
{
"orderId": "ORD-9912",
"customerId": "CUST-001",
"items": [
{"productId": "PROD-1", "quantity": 2}
],
"totalAmount": 299.90,
"stockReservationId": "RES-123",
"paymentIntentId": "pi_abc123",
"shippingTrackingId": null
}
Usage in Steps
Invoke
private OrderContext reserveStock(OrderContext ctx) {
// Uses data from context
String reservationId = stockService.reserve(ctx.items());
// Returns NEW context with the result
return ctx.withStockReservationId(reservationId);
}
Compensate
private void releaseStock(OrderContext ctx) {
// Uses the previously saved ID
stockService.release(ctx.stockReservationId());
}
Kotlin
Using Kotlin? Extend KSagaContext instead of implementing SagaContext. It replaces Optional<String> with idiomatic String?:
// Java SagaContext requires Optional
override fun businessKey() = Optional.of(orderId.toString()) // ❌
// KSagaContext — plain String?
data class OrderContext(val orderId: UUID) : KSagaContext() {
override fun key() = orderId.toString() // ✅
}
See the Kotlin guide → for the full setup.