Skip to main content

StepOutput

Pattern for capturing data from a step for use in compensations.


Concept

StepOutput is a pattern for saving IDs, tokens, or any data needed to revert an operation. This data is stored in the context.


Example

Problem

private OrderContext processPayment(OrderContext ctx) {
// Stripe returns a payment ID
PaymentIntent intent = stripe.createPaymentIntent(ctx.totalAmount());

// <DocIcon name="brain" /> Where to save the intent.id to use in compensation?
}

Solution

private OrderContext processPayment(OrderContext ctx) {
PaymentIntent intent = stripe.createPaymentIntent(ctx.totalAmount());

// <DocIcon name="check" /> Saves in context for compensation
return ctx.withPaymentIntentId(intent.getId());
}

private void refundPayment(OrderContext ctx) {
// <DocIcon name="check" /> Uses the saved ID
stripe.refund(ctx.paymentIntentId());
}

Structured Pattern

For complex sagas, you can use a dedicated structure:

public record OrderContext(
// Input data
String orderId,
BigDecimal amount,

// Step outputs organized
StepOutputs outputs
) {
public OrderContext withOutput(String key, String value) {
return new OrderContext(orderId, amount, outputs.with(key, value));
}
}

public record StepOutputs(Map<String, String> data) {
public static StepOutputs empty() {
return new StepOutputs(Map.of());
}

public StepOutputs with(String key, String value) {
var newData = new HashMap<>(data);
newData.put(key, value);
return new StepOutputs(Map.copyOf(newData));
}

public String get(String key) {
return data.get(key);
}
}

Usage

private OrderContext processPayment(OrderContext ctx) {
PaymentIntent intent = stripe.createPaymentIntent(ctx.amount());
return ctx.withOutput("paymentIntentId", intent.getId());
}

private void refundPayment(OrderContext ctx) {
String intentId = ctx.outputs().get("paymentIntentId");
stripe.refund(intentId);
}

When to Use

ScenarioWhat to save
PaymentpaymentIntentId, chargeId
Stock reservationreservationId
Balance blockblockId
Token validationvalidationToken
SchedulingscheduleId

Best Practices

Do

  • Save only IDs and tokens
  • Use primitive types (String, Long)
  • Name clearly (e.g. paymentIntentId, not id)

Avoid

  • Saving entire complex objects
  • Depending on external state
  • Assuming the output exists (validate it!)