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:
@Transactionalnão funciona entre microserviços- Ordem de compensação manual = bugs
- Sem persistência = se reiniciar, perde o estado
- Sem retry = falhas intermitentes viram permanentes
- 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: