Step Types
Sagaweaw automatically infers the type of each step based on its definition. There are three types:
COMPENSABLE
A COMPENSABLE step has a compensate() method defined. If a subsequent step fails, Sagaweaw will execute the compensation automatically.
.step("block-balance")
.invoke(this::blockBalance)
.compensate(this::unblockBalance) // ← Defines compensation
Characteristics
| Aspect | Behavior |
|---|---|
| Compensation | Executed in reverse order |
| Data | Context available for compensation |
| Retry | Configurable |
Full Example
// Invoke: Block the balance
private PixContext blockBalance(PixContext ctx) {
String blockId = balanceService.block(ctx.payerId(), ctx.amount());
return ctx.withBlockId(blockId); // Save the ID for compensation
}
// Compensate: Unblock using the saved blockId
private void unblockBalance(PixContext ctx) {
balanceService.unblock(ctx.blockId()); // Uses the saved ID
}
Always save in the context any data needed for compensation (IDs, tokens, etc).
PIVOT
A PIVOT step is the "point of no return". It has no compensation defined. After a PIVOT executes successfully, previous steps will not be compensated even if subsequent steps fail.
.step("transmit-to-bacen")
.invoke(this::transmitToBacen)
// No .compensate() = PIVOT
Characteristics
| Aspect | Behavior |
|---|---|
| Compensation | Does not exist |
| Point of no return | Yes |
| Retry | Generally configured |
When to Use
- Irreversible operations: Sending email, SMS, push notification
- External integrations without rollback: Third-party APIs, BACEN, gateways
- Final confirmations: External transaction commit
Example
.step("transmit-to-bacen") // PIVOT
.invoke(this::transmitToBacen)
.retry(exponential(3, Duration.ofSeconds(1))) // Retry before giving up
If a PIVOT fails after all retry attempts, the saga enters FAILED state and previous COMPENSABLE steps are compensated.
RETRIABLE
A RETRIABLE step has infinite retry and no compensation. It must eventually always succeed. Use for idempotent operations that may fail temporarily.
.step("send-notification")
.invoke(this::sendNotification)
.retry(infinite()) // ← Infinite retry without compensate = RETRIABLE
Characteristics
| Aspect | Behavior |
|---|---|
| Compensation | Does not exist |
| Retry | Infinite |
| Expectation | Will always succeed |
When to Use
- Notifications: Email, SMS, push (idempotent)
- Cache updates: Eventual consistency
- Webhooks: Notifying external systems
Example
.step("send-confirmation-email")
.invoke(this::sendConfirmationEmail)
.retry(infinite(Duration.ofMinutes(5), Duration.ofHours(1))) // Min 5min, max 1h between attempts
Only use RETRIABLE if the operation is idempotent (executing multiple times produces the same result).
Comparison Table
| Type | compensate() | retry() | When to Use |
|---|---|---|---|
| COMPENSABLE | Defined | Optional | Reversible operations |
| PIVOT | None | Recommended | Point of no return |
| RETRIABLE | None | infinite() | Must always succeed |
Inference Flow
Sagaweaw determines the type automatically:
┌─────────────────────────────────────────┐
│ Step defined │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────┐
│ Has compensate? │
└────────┬────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
COMPENSABLE ┌─────────────────┐
│ Infinite retry? │
└────────┬────────┘
│
┌───────┴───────┐
│ │
YES NO
│ │
▼ ▼
RETRIABLE PIVOT