Pular para o conteúdo principal

O Problema

A Analogia do Restaurante

Imagine que você está em um restaurante e faz um pedido:

1. Garçom anota o pedido ✓
2. Cozinha recebe o pedido ✓
3. Cozinha prepara o prato ✓
4. Você paga a conta ✓
5. Pedido entregue ✓

Parece simples, certo? Mas o que acontece se a cozinha queima o prato no passo 3?

1. Garçom anota o pedido ✓
2. Cozinha recebe o pedido ✓
3. Cozinha prepara o prato ✗ FALHOU!
└─ ↩️ Cancelar reserva de ingredientes
└─ ↩️ Notificar cliente sobre atraso
4. Você paga a conta (não acontece)
5. Pedido entregue (não acontece)

As ações precisam ser desfeitas em ordem inversa.


O Problema Real em Sistemas Distribuídos

Em sistemas reais, a mesma lógica se aplica. Considere um pagamento PIX:

// Fluxo ideal
1. validateDict() // Validar chave PIX ✓
2. blockBalance() // Bloquear saldo ✓
3. transmitToBacen() // Enviar ao BACEN ✓
4. confirmTransaction() // Confirmar ✓

Mas se o BACEN timeout no passo 3:

1. validateDict() // ✓
2. blockBalance() // ✓
3. transmitToBacen() // ✗ TIMEOUT!
└─ ↩️ unblockBalance() // Liberar o saldo
└─ ↩️ invalidateDict() // Invalidar a validação

Por que é Difícil?

Sem Sagaweaw

@Service
public class PaymentService {

@Transactional // Não funciona entre serviços!
public void processPayment(Request req) {
try {
dictService.validate(req.getKey());
balanceService.block(req.getAmount());
bacenService.transmit(req.getTransaction());
} catch (BacenTimeoutException e) {
// <DocIcon name="alert" /> E agora? Como desfazer?
// Em que ordem?
// E se unblockBalance também falhar?
// E se o sistema reiniciar no meio?
}
}
}

Problemas:

  1. @Transactional não funciona entre microserviços
  2. Ordem de compensação manual = bugs
  3. Sem persistência = se reiniciar, perde o estado
  4. Sem retry = falhas intermitentes viram permanentes
  5. Sem visibilidade = "o que aconteceu com aquele pagamento?"

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

O Sagaweaw cuida de:

  • Ordem inversa de compensação automática
  • Persistência do estado em PostgreSQL
  • Retry com backoff exponencial
  • Idempotência nativa
  • Observabilidade via REST API (/api/sagas, /api/dead-letters)

Visualização via API

Quando algo falha, uma chamada a GET /api/sagas/{id} mostra exatamente o que aconteceu:

{
"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
}
]
}

Próximo Passo

Agora que você entende o problema, vamos colocar a mão na massa:

Quickstart →