Immutable Context
The SagaContext is the object that travels between steps. It must be treated as immutable to ensure predictability and auditability.
Why Immutability?
Without Immutability ( Wrong)
public class PixContext {
private String blockId; // Mutable
public void setBlockId(String id) {
this.blockId = id; // <DocIcon name="alert" /> Mutation!
}
}
// In the step...
private PixContext blockBalance(PixContext ctx) {
String blockId = balanceService.block(...);
ctx.setBlockId(blockId); // <DocIcon name="alert" /> Modifies the original object
return ctx;
}
Problems:
- Hard to track when the value changed
- Race conditions in concurrent scenarios
- Compensation may see an inconsistent value
- Audit log does not reflect real state
With Immutability ( Correct)
public record PixContext(
String transactionId,
String dictKey,
BigDecimal amount,
String blockId // Immutable
) {
public PixContext withBlockId(String id) {
return new PixContext(transactionId, dictKey, amount, id);
}
}
// In the step...
private PixContext blockBalance(PixContext ctx) {
String blockId = balanceService.block(...);
return ctx.withBlockId(blockId); // <DocIcon name="check" /> Returns new instance
}
Benefits:
- Each step receives a snapshot of the state
- Easy to debug and audit
- Thread-safe by design
- Compensation has access to the correct state
Implementation Patterns
Using Java Records (Recommended)
public record OrderContext(
// Input data (immutable from the start)
String orderId,
String customerId,
List<OrderItem> items,
BigDecimal totalAmount,
// Step outputs (populated during execution)
String stockReservationId,
String paymentIntentId,
String shippingLabelId
) {
// Builders for each output
public OrderContext withStockReservationId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
id, paymentIntentId, shippingLabelId
);
}
public OrderContext withPaymentIntentId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
stockReservationId, id, shippingLabelId
);
}
public OrderContext withShippingLabelId(String id) {
return new OrderContext(
orderId, customerId, items, totalAmount,
stockReservationId, paymentIntentId, id
);
}
}
Using Lombok @With
@Value
@With
public class OrderContext {
String orderId;
String customerId;
List<OrderItem> items;
BigDecimal totalAmount;
String stockReservationId;
String paymentIntentId;
String shippingLabelId;
}
// Usage
return ctx.withStockReservationId(id);
Using Builder Pattern
@Value
@Builder(toBuilder = true)
public class OrderContext {
String orderId;
String customerId;
List<OrderItem> items;
BigDecimal totalAmount;
String stockReservationId;
String paymentIntentId;
String shippingLabelId;
}
// Usage
return ctx.toBuilder()
.stockReservationId(id)
.build();
Context Flow
┌─────────────────────────────────────────────────────────────────┐
│ Saga Execution │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Context v0 ─────► Step 1 ─────► Context v1 │
│ {orderId, ...} reserve() {orderId, stockId, ...} │
│ │
│ Context v1 ─────► Step 2 ─────► Context v2 │
│ {stockId, ...} charge() {stockId, paymentId, ...} │
│ │
│ Context v2 ─────► Step 3 ─────► Context v3 (final) │
│ {paymentId,...} ship() {paymentId, trackingId,...} │
│ │
└─────────────────────────────────────────────────────────────────┘
Compensation (if step 3 fails):
│ Context v2 ─────► Comp 2 ─────► refund(paymentId) │
│ Context v1 ─────► Comp 1 ─────► release(stockId) │
Context Persistence
Sagaweaw serializes the context as JSONB in PostgreSQL:
SELECT id, name, status, context
FROM sagas
WHERE id = 'saga-48291';
{
"orderId": "ORD-9912",
"customerId": "CUST-001",
"items": [...],
"totalAmount": 299.90,
"stockReservationId": "STK-123",
"paymentIntentId": "pi_abc123",
"shippingLabelId": null
}
Serialization
Use serializable types (String, BigDecimal, List, Map). Avoid complex objects or circular references.
Common Mistakes
Mutable Collections
// WRONG
public record OrderContext(
List<String> processedIds // ArrayList is mutable!
) {}
ctx.processedIds().add("new-id"); // <DocIcon name="alert" /> Mutation!
Immutable Collections
// CORRECT
public record OrderContext(
List<String> processedIds
) {
public OrderContext withAddedId(String id) {
var newList = new ArrayList<>(processedIds);
newList.add(id);
return new OrderContext(List.copyOf(newList));
}
}