Skip to main content

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

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));
}
}

Next Steps