Pular para o conteúdo principal

Quickstart

Configure o Sagaweaw em seu projeto Spring Boot em 5 minutos.


1. Adicione a Dependência

Maven

<dependency>
<groupId>dev.sagaweaw</groupId>
<artifactId>sagaweaw-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Flyway é obrigatório para criação automática do schema -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Se usar PostgreSQL, adicione também: -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>runtime</scope>
</dependency>

Gradle

implementation 'dev.sagaweaw:sagaweaw-spring-boot-starter:1.0.0'
runtimeOnly 'org.flywaydb:flyway-core'
runtimeOnly 'org.flywaydb:flyway-database-postgresql' // se usar PostgreSQL

2. Configure o Banco de Dados

O Sagaweaw suporta PostgreSQL, MySQL 8+ e H2 — o Flyway seleciona automaticamente a migration correta via {vendor}. Adicione no application.yml:

spring:
datasource:
url: jdbc:postgresql://localhost:5432/sagaweaw # ou mysql:// ou h2
username: ${DB_USER}
password: ${DB_PASSWORD}

sagaweaw:
enabled: true
observability:
enabled: true
token: ${SAGAWEAW_TOKEN} # obrigatório para acessar /api/sagas e /api/dead-letters
retry:
default-policy: exponential
max-attempts: 3
Migrations automáticas

O starter executa as migrations via Flyway ao iniciar. As tabelas sagas, saga_steps, saga_events, outbox_messages e dead_letters são criadas automaticamente para o banco detectado.

Token de segurança

Sem sagaweaw.observability.token configurado, todos os endpoints de observabilidade retornam 403. Isso é intencional.


3. Crie o Context

O SagaContext é um objeto imutável que viaja entre os steps:

public record PixContext(
String transactionId,
String dictKey,
BigDecimal amount,
String payerId,
String payeeId,

// Outputs dos steps (populados durante execução)
String validationToken,
String blockId,
String bacenProtocol
) {
// Builder para criar novas versões imutáveis
public PixContext withValidationToken(String token) {
return new PixContext(
transactionId, dictKey, amount, payerId, payeeId,
token, blockId, bacenProtocol
);
}

public PixContext withBlockId(String id) {
return new PixContext(
transactionId, dictKey, amount, payerId, payeeId,
validationToken, id, bacenProtocol
);
}
}

4. Defina a Saga

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

private final DictService dictService;
private final BalanceService balanceService;
private final BacenService bacenService;

@Override
public SagaFlow<PixContext> define(SagaBuilder<PixContext> saga) {
return saga
// Step 1: Validar chave PIX
.step("validate-dict")
.invoke(this::validateDict)
.compensate(this::invalidateDict)

// Step 2: Bloquear saldo
.step("block-balance")
.invoke(this::blockBalance)
.compensate(this::unblockBalance)

// Step 3: Transmitir ao BACEN (PIVOT - sem compensação)
.step("transmit-to-bacen")
.invoke(this::transmitToBacen)
.retry(exponential(3, Duration.ofSeconds(1)))

.build();
}

// --- Invoke Methods ---

private PixContext validateDict(PixContext ctx) {
String token = dictService.validate(ctx.dictKey());
return ctx.withValidationToken(token);
}

private PixContext blockBalance(PixContext ctx) {
String blockId = balanceService.block(ctx.payerId(), ctx.amount());
return ctx.withBlockId(blockId);
}

private PixContext transmitToBacen(PixContext ctx) {
bacenService.transmit(ctx.transactionId(), ctx.amount());
return ctx;
}

// --- Compensate Methods ---

private void invalidateDict(PixContext ctx) {
dictService.invalidate(ctx.validationToken());
}

private void unblockBalance(PixContext ctx) {
balanceService.unblock(ctx.blockId());
}
}

5. Dispare a Saga

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

private final SagaManager sagaManager;

@PostMapping("/pix")
public ResponseEntity<SagaResponse> createPixPayment(
@RequestBody PixRequest request
) {
PixContext context = new PixContext(
UUID.randomUUID().toString(),
request.getDictKey(),
request.getAmount(),
request.getPayerId(),
request.getPayeeId(),
null, null, null // Outputs iniciam nulos
);

SagaExecution execution = sagaManager.start(PixPaymentSaga.class, context);

return ResponseEntity.accepted().body(
new SagaResponse(execution.sagaId())
);
}
}

6. Acesse a Observability API

Com a aplicação rodando, consulte suas sagas via REST:

# Listar sagas (últimas 50)
curl -H "Authorization: Bearer $SAGAWEAW_TOKEN" \
http://localhost:8080/api/sagas

# Métricas agregadas
curl -H "Authorization: Bearer $SAGAWEAW_TOKEN" \
http://localhost:8080/api/sagas/metrics

Você também pode usar o header X-Sagaweaw-Token: <token> como alternativa ao Bearer.

Documentação completa

Veja todos os endpoints disponíveis em Observability API →.


Verificação

Sua estrutura deve estar assim:

src/main/java/
├── payment/
│ ├── PixPaymentSaga.java # Definição da saga — junto ao domínio
│ ├── PixContext.java # Context imutável
│ ├── PaymentController.java # Endpoint de disparo
│ └── service/
│ ├── DictService.java
│ ├── BalanceService.java
│ └── BacenService.java
Onde colocar a saga

Mantenha PixPaymentSaga no mesmo pacote dos serviços que ela usa. Evite criar um pacote sagas/ separado — isso dificulta entender o contexto ao ler o código.


Próximos Passos