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
| Scenario | What to save |
|---|---|
| Payment | paymentIntentId, chargeId |
| Stock reservation | reservationId |
| Balance block | blockId |
| Token validation | validationToken |
| Scheduling | scheduleId |
Best Practices
Do
- Save only IDs and tokens
- Use primitive types (String, Long)
- Name clearly (e.g.
paymentIntentId, notid)
Avoid
- Saving entire complex objects
- Depending on external state
- Assuming the output exists (validate it!)