Quickstart
In 5 minutes you'll have a saga running with automatic compensation. No new cluster. No new concepts.
This quickstart uses Java. For the idiomatic Kotlin DSL — no Consumer<T>, no ::class.java — see the Kotlin guide →.
1. Add the Dependency
Maven
<dependency>
<groupId>dev.sagaweaw</groupId>
<artifactId>sagaweaw-spring-boot-starter</artifactId>
<version>1.0.14</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<scope>runtime</scope>
</dependency>
<!-- PostgreSQL users also add: -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Gradle
implementation 'dev.sagaweaw:sagaweaw-spring-boot-starter:1.0.14'
runtimeOnly 'org.flywaydb:flyway-core'
runtimeOnly 'org.flywaydb:flyway-database-postgresql' // PostgreSQL only
2. Configure
Add to application.properties:
sagaweaw.observability.token=my-local-token
sagaweaw.kafka.enabled=false
# Optional: allow the standalone dashboard (port 8484) to call the API from the browser
# sagaweaw.observability.cors.allowed-origins=http://localhost:8484
Sagaweaw creates its own tables (sagas, saga_steps, saga_events, dead_letters) automatically via embedded Flyway on startup. No manual SQL needed.
Without sagaweaw.observability.token, every observability endpoint returns 403. Any string works for local development.
3. Write the Saga
Define your steps and compensations. That's all.
@Saga("pix-payment")
@Component
public class PixPaymentSaga implements SagaDefinition<PixPaymentSaga.Context> {
private final DictService dictService;
private final BalanceService balanceService;
private final BacenService bacenService;
public record Context(
String transactionId,
String dictKey,
BigDecimal amount,
String payerId,
String validationToken, // filled by step 1
String blockId // filled by step 2
) implements SagaContext {
@Override public Optional<String> businessKey() {
return Optional.of(transactionId);
}
}
@Override
public SagaFlow<Context> define(SagaBuilder<Context> saga) {
return saga
.step("validate-dict")
.invoke(ctx -> dictService.validate(ctx.dictKey()))
.compensate(ctx -> dictService.invalidate(ctx.validationToken()))
.step("block-balance")
.invoke(ctx -> balanceService.block(ctx.payerId(), ctx.amount()))
.compensate(ctx -> balanceService.unblock(ctx.blockId()))
.step("transmit-to-bacen") // PIVOT — no compensation
.invoke(ctx -> bacenService.transmit(ctx.transactionId(), ctx.amount()))
.retryPolicy(RetryPolicy.exponential(3, Duration.ofSeconds(1)))
.build();
}
}
4. See It in the Dashboard
Two ways to fire a saga for the first time. Choose the one that fits your case.
Case 1 — Saga with no real database dependencies
Add @AutoStart and implement SagaSampler<Context>. The saga fires automatically when the application starts.
@Saga("pix-payment")
@AutoStart // fires on startup
@Component
public class PixPaymentSaga
implements SagaDefinition<PixPaymentSaga.Context>,
SagaSampler<PixPaymentSaga.Context> { // provides sample data
@Override
public Context sampleContext() {
return new Context(
"txn-sample-001", // transactionId
"joao@pix.com", // dictKey
new BigDecimal("150.00"),
"user-42", // payerId
null, null
);
}
// ... define() unchanged
}
Enable in application.properties (dev only — never set this in production):
sagaweaw.auto-start.enabled=true
Start the application. Open http://localhost:8080/sagaweaw. The saga appears in the dashboard immediately.
Prefer hot reload? Run npm run dev in sagaweaw-dashboard/ and open http://localhost:8484 instead. See Local development →.
sagaweaw.auto-start.enabled defaults to false. The runner only fires when you explicitly turn it on. Never set it in a production profile.
Case 2 — Saga that needs real data from the database
Use the built-in trigger endpoint. First, discover the context schema:
curl -H "X-Sagaweaw-Token: my-local-token" \
http://localhost:8080/api/sagas/registry
[{
"name": "pix-payment",
"contextFields": {
"transactionId": "String",
"dictKey": "String",
"amount": "BigDecimal",
"payerId": "String"
},
"sampleContext": null
}]
Then trigger with real IDs from your database:
curl -X POST \
-H "X-Sagaweaw-Token: my-local-token" \
-H "Content-Type: application/json" \
-d '{"transactionId":"txn-001","dictKey":"joao@pix.com","amount":150.00,"payerId":"user-42"}' \
http://localhost:8080/api/sagas/trigger/pix-payment
{
"sagaId": "f3a1c2d4-...",
"sagaName": "pix-payment",
"startedAt": "2025-01-15T14:32:00Z",
"idempotent": false
}
Open http://localhost:8080/sagaweaw and watch each step execute in real time.
/api/sagas/registry and /api/sagas/trigger/{sagaName} ship with the starter. No extra code required.
5. Wire to Production
The trigger endpoint and @AutoStart are for development. In production, inject SagaManager at the business event and call start():
@Service
public class PaymentService {
private final SagaManager sagaManager;
public void processPayment(PaymentRequest request) {
var ctx = new PixPaymentSaga.Context(
UUID.randomUUID().toString(),
request.getDictKey(),
request.getAmount(),
request.getPayerId(),
null, null
);
sagaManager.start(PixPaymentSaga.class, ctx);
}
}
One injection. One call. Retries, compensation, and observability are automatic.
Next Steps
- Production → — Deploy to production in 5 steps
- Local development → — Vite dev server, @AutoStart, SagaSampler
- Step Types → — COMPENSABLE, PIVOT, RETRIABLE
- Lifecycle → — Saga states
- Kotlin → — Idiomatic DSL with no
Consumer<T> - AI Prompt → — Generate sagas with AI