Skip to main content

Quickstart

In 5 minutes you'll have a saga running with automatic compensation. No new cluster. No new concepts.

Using Kotlin?

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.

Token required

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.

Standalone dev server

Prefer hot reload? Run npm run dev in sagaweaw-dashboard/ and open http://localhost:8484 instead. See Local development →.

Production safety

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.

Both endpoints are built in

/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