Skip to main content

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.