Exemplo: PIX Payment
Um exemplo completo de saga para processamento de pagamentos PIX.
Fluxo
┌─────────────────────────────────────────────────────────────────┐
│ PIX Payment Saga │
├────────────────────────── ───────────────────────────────────────┤
│ │
│ 1. validate-dict → COMPENSABLE → invalidateDict() │
│ └─ Valida chave PIX no DICT │
│ │
│ 2. block-balance → COMPENSABLE → unblockBalance() │
│ └─ Bloqueia saldo do pagador │
│ │
│ 3. transmit-to-bacen → PIVOT (sem compensação) │
│ └─ Transmite ao BACEN via SPI │
│ │
│ 4. confirm-payment → RETRIABLE │
│ └─ Confirma e notifica │
│ │
└─────────────────────────────────────────────────────────────────┘
Context
public record PixContext(
// Dados de entrada
String transactionId,
String dictKey,
BigDecimal amount,
String payerId,
String payeeId,
String description,
// Outputs dos steps
String validationToken,
String blockId,
String bacenProtocol
) {
public static PixContext create(PixRequest request) {
return new PixContext(
UUID.randomUUID().toString(),
request.getDictKey(),
request.getAmount(),
request.getPayerId(),
request.getPayeeId(),
request.getDescription(),
null, null, null
);
}
public PixContext withValidationToken(String token) {
return new PixContext(
transactionId, dictKey, amount, payerId, payeeId, description,
token, blockId, bacenProtocol
);
}
public PixContext withBlockId(String id) {
return new PixContext(
transactionId, dictKey, amount, payerId, payeeId, description,
validationToken, id, bacenProtocol
);
}
public PixContext withBacenProtocol(String protocol) {
return new PixContext(
transactionId, dictKey, amount, payerId, payeeId, description,
validationToken, blockId, protocol
);
}
}
Saga Definition
@Saga(
value = "pix-payment",
description = "Processa pagamentos PIX com compensação automática"
)
public class PixPaymentSaga implements SagaDefinition<PixContext> {
private final DictService dictService;
private final BalanceService balanceService;
private final BacenService bacenService;
private final NotificationService notificationService;
public PixPaymentSaga(
DictService dictService,
BalanceService balanceService,
BacenService bacenService,
NotificationService notificationService
) {
this.dictService = dictService;
this.balanceService = balanceService;
this.bacenService = bacenService;
this.notificationService = notificationService;
}
@Override
public void define(SagaBuilder<PixContext> builder) {
builder
// Step 1: Validar chave PIX (COMPENSABLE)
.step("validate-dict")
.invoke(this::validateDict)
.compensate(this::invalidateDict)
.timeout(Duration.ofSeconds(5))
// Step 2: Bloquear saldo (COMPENSABLE)
.step("block-balance")
.invoke(this::blockBalance)
.compensate(this::unblockBalance)
.timeout(Duration.ofSeconds(5))
// Step 3: Transmitir ao BACEN (PIVOT)
.step("transmit-to-bacen")
.invoke(this::transmitToBacen)
.retry(exponential(3, Duration.ofSeconds(2)))
.timeout(Duration.ofSeconds(30))
// Step 4: Confirmar e notificar (RETRIABLE)
.step("confirm-payment")
.invoke(this::confirmPayment)
.retry(infinite())
.build();
}
// ═══════════════════════════════════════════════════════════════
// INVOKE METHODS
// ═══════════════════════════════════════════════════════════════
private PixContext validateDict(PixContext ctx) {
log.info("Validating DICT key: {}", ctx.dictKey());
DictValidation validation = dictService.validate(ctx.dictKey());
if (!validation.isValid()) {
throw new InvalidDictKeyException(ctx.dictKey());
}
return ctx.withValidationToken(validation.getToken());
}
private PixContext blockBalance(PixContext ctx) {
log.info("Blocking balance: {} for payer: {}", ctx.amount(), ctx.payerId());
String blockId = balanceService.block(
ctx.payerId(),
ctx.amount(),
ctx.transactionId()
);
return ctx.withBlockId(blockId);
}
private PixContext transmitToBacen(PixContext ctx) {
log.info("Transmitting to BACEN: {}", ctx.transactionId());
BacenResponse response = bacenService.transmit(
BacenRequest.builder()
.transactionId(ctx.transactionId())
.amount(ctx.amount())
.payerId(ctx.payerId())
.payeeId(ctx.payeeId())
.dictKey(ctx.dictKey())
.build()
);
return ctx.withBacenProtocol(response.getProtocol());
}
private PixContext confirmPayment(PixContext ctx) {
log.info("Confirming payment: {}", ctx.transactionId());
// Libera o bloqueio transformando em débito efetivo
balanceService.confirmBlock(ctx.blockId());
// Notifica o pagador
notificationService.sendPaymentConfirmation(
ctx.payerId(),
ctx.transactionId(),
ctx.amount()
);
// Notifica o recebedor
notificationService.sendPaymentReceived(
ctx.payeeId(),
ctx.transactionId(),
ctx.amount()
);
return ctx;
}
// ═══════════════════════════════════════════════════════════════
// COMPENSATE METHODS
// ═══════════════════════════════════════════════════════════════
private void invalidateDict(PixContext ctx) {
log.info("Compensating: invalidating DICT validation: {}", ctx.validationToken());
if (ctx.validationToken() != null) {
dictService.invalidate(ctx.validationToken());
}
}
private void unblockBalance(PixContext ctx) {
log.info("Compensating: unblocking balance: {}", ctx.blockId());
if (ctx.blockId() != null) {
balanceService.unblock(ctx.blockId());
}
}
}
Controller
@RestController
@RequestMapping("/api/pix")
public class PixController {
private final SagaManager sagaManager;
@PostMapping
public ResponseEntity<PixResponse> createPixPayment(
@RequestBody @Valid PixRequest request,
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey
) {
PixContext context = PixContext.create(request);
SagaExecution execution = sagaManager.start(
PixPaymentSaga.class,
context,
idempotencyKey != null
? IdempotencyKey.of(idempotencyKey)
: IdempotencyKey.generate()
);
// Retorna o sagaId — cliente consulta status via GET /api/sagas/{id}
return ResponseEntity.accepted().body(
new PixResponse(execution.sagaId(), context.transactionId(), execution.idempotent())
);
}
}
Visualização via API
Ao consultar GET /api/sagas/{id}, você obtém:
┌─────────────────────────────────────────────────────────────────┐
│ pix-payment · #saga-48291 [COMPENSATED] │
│ Iniciada 14:32:01 · Duração total 3.4s · tx #PIX-123 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ● validate-dict 120ms ✓ COMPLETED → ↩️ COMPENSATED │
│ │ │
│ ● block-balance 89ms ✓ COMPLETED → ↩️ COMPENSATED │
│ │ │
│ ✗ transmit-to-bacen 10s ✗ FAILED (timeout) │
│ └─ io.sagaweaw.StepTimeoutException │
│ └─ Endpoint: bacen.gov.br/spi/v2 · HTTP 504 │
│ └─ Tentativas: 3/3 │
│ └─ → Compensação iniciada em ordem inversa │
│ │
└─────────────────────────────────────────────────────────────────┘