Skip to main content

The Problem

The Restaurant Analogy

Imagine you're at a restaurant and place an order:

1. Waiter takes the order ✓
2. Kitchen receives the order ✓
3. Kitchen prepares the dish ✓
4. You pay the bill ✓
5. Order delivered ✓

Seems simple, right? But what happens if the kitchen burns the dish at step 3?

1. Waiter takes the order ✓
2. Kitchen receives the order ✓
3. Kitchen prepares the dish ✗ FAILED!
└─ ↩️ Cancel ingredient reservation
└─ ↩️ Notify customer of delay
4. You pay the bill (doesn't happen)
5. Order delivered (doesn't happen)

Actions need to be undone in reverse order.


The Real Problem in Distributed Systems

In real systems, the same logic applies. Consider a PIX payment:

// Ideal flow
1. validateDict() // Validate PIX key ✓
2. blockBalance() // Block balance ✓
3. transmitToBacen() // Send to BACEN ✓
4. confirmTransaction() // Confirm ✓

But if BACEN times out at step 3:

1. validateDict() // ✓
2. blockBalance() // ✓
3. transmitToBacen() // ✗ TIMEOUT!
└─ ↩️ unblockBalance() // Release the balance
└─ ↩️ invalidateDict() // Invalidate the validation

Why Is This Hard?

Without Sagaweaw

@Service
public class PaymentService {

@Transactional // Doesn't work across services!
public void processPayment(Request req) {
try {
dictService.validate(req.getKey());
balanceService.block(req.getAmount());
bacenService.transmit(req.getTransaction());
} catch (BacenTimeoutException e) {
// <DocIcon name="alert" /> Now what? How do we undo?
// In what order?
// What if unblockBalance also fails?
// What if the system restarts mid-rollback?
}
}
}

Problems:

  1. @Transactional doesn't work across microservices
  2. Manual compensation order = bugs
  3. No persistence = if the system restarts, state is lost
  4. No retry = intermittent failures become permanent
  5. No visibility = "what happened to that payment?"

With Sagaweaw

@Saga("pix-payment")
public class PixPaymentSaga implements SagaDefinition<PixContext> {

@Override
public void define(SagaBuilder<PixContext> builder) {
builder
.step("validate-dict")
.invoke(this::validateDict)
.compensate(this::invalidateDict) // ↩️ Auto

.step("block-balance")
.invoke(this::blockBalance)
.compensate(this::unblockBalance) // ↩️ Auto

.step("transmit-to-bacen") // PIVOT
.invoke(this::transmitToBacen)
.retry(exponential(3, Duration.ofSeconds(1)))

.build();
}
}

Sagaweaw handles:

  • Reverse order automatic compensation
  • State persistence in PostgreSQL
  • Retry with exponential backoff
  • Native idempotency
  • Observability via REST API (/api/sagas, /api/dead-letters)

Visibility via API

When something fails, a call to GET /api/sagas/{id} shows exactly what happened:

{
"id": "saga-48291",
"name": "pix-payment",
"status": { "type": "COMPENSATED" },
"steps": [
{
"name": "validate-dict",
"status": { "type": "COMPENSATED" },
"attempt": 1, "maxAttempts": 3,
"durationMs": 120
},
{
"name": "block-balance",
"status": { "type": "COMPENSATED" },
"attempt": 1, "maxAttempts": 3,
"durationMs": 89
},
{
"name": "transmit-to-bacen",
"status": { "type": "FAILED" },
"attempt": 3, "maxAttempts": 3,
"lastError": "HTTP 504 · bacen.gov.br/spi/v2",
"errorTrace": "io.sagaweaw.StepTimeoutException: ...",
"durationMs": 10000
}
]
}

Next Step

Now that you understand the problem, let's get our hands dirty:

Quickstart →