SAGAs
A saga (also called a process manager) coordinates a long-running, multi-step business process that spans several aggregates and transactions. Rather than locking all participants inside a single distributed transaction, a saga reacts to domain events, issues follow-up commands, and — when needed — schedules timeouts, driving the process to completion or compensation one step at a time.
Banking example
Consider a funds transfer between two accounts.
The transfer must debit one account and credit another, but the two accounts live in separate aggregates with separate transactions.
A TransferSaga listens for a TransferRequested event, dispatches a DebitAccount command, and — once the debit confirms — dispatches a CreditAccount command.
If the credit fails or a deadline fires, the saga can compensate by reversing the debit.
No two-phase commit required.
The Spring DDD model
Annotate a plain Kotlin class with @Saga to declare it as a process manager:
@Saga(type = "transfer-saga") (1)
class TransferSaga {
var transferId: String? = null (2)
var state: TransferState = TransferState.PENDING
@SagaEventHandler(associationProperty = "transferId") (3)
@SagaStart
fun on(event: TransferRequested) {
this.transferId = event.transferId
// dispatch debit command...
}
}
| 1 | type is the stable identifier stored in the database; leave blank to default to the fully-qualified class name. |
| 2 | Saga state is persisted as a JSON blob — the class must be a non-data class with mutable var fields and a no-argument constructor so it round-trips cleanly through JSON. |
| 3 | associationProperty names the event property whose runtime value is the correlation id; Spring DDD resolves the saga instance (or creates one, if @SagaStart is present) for that value. |
A @Saga class is not a Spring bean — instances are managed by the framework, not the Spring container.
Each instance is identified by a (sagaType, sagaId) pair, where sagaId is the association value coerced to a string via toString().
Sagas are independent of event sourcing and CQRS. They react to the events the topics carry, event-sourced or captured.
Delivery model
A saga reads its events from topics: the event store topic (events of @EventSourced aggregates) and the capture topic (captured `@DomainEvent`s), through one consumer per saga type.
Delivery is at-least-once, and one saga instance applies one message at a time.
See Event delivery.
Lifecycle
A saga instance moves through three stages:
-
Created — a
@SagaStartevent handler creates a new instance (or routes to an existing one if a duplicate arrives). -
Active — subsequent
@SagaEventHandlerevents update the instance state; commands and deadlines may be issued from within these handlers. -
Completed — a
@SagaEndannotation on the terminal handler, or an explicit call to the injectedSagaLifecycle.end(), tombstones the instance. Late re-deliveries of events to a completed saga are silently skipped — the instance is not resurrected.
For full details on wiring start, end, and conditional completion, see Writing a saga.
Starters and dependencies
The minimal dependency for a runnable saga is:
// build.gradle.kts
implementation("de.dwittkoetter:spring-ddd-starter-saga-jdbc:0.0.1-SNAPSHOT")
A saga reads the captured @DomainEvent`s with this starter alone.
To react to the events of `@EventSourced aggregates too, add an event store; the saga then reads its topic as well, with no further dependency:
implementation("de.dwittkoetter:spring-ddd-starter-eventsourcing-jdbc:0.0.1-SNAPSHOT") // the JDBC event store
The full-stack bundle spring-ddd-starter-jpa already includes the saga runtime and an event store; no extra dependency is needed when using it.
For observability, add spring-ddd-starter-observability — see Observability.
For guidance on choosing the right starter combination, see Starters.
In this section
-
Writing a saga —
@Saga/@SagaEventHandler/@SagaStart/@SagaEnd, state design, handler parameters, idempotency. -
Event delivery — the topics a saga reads, one consumer per saga type, delivery guarantees, scaling, errors.
-
Deadlines & timeouts — scheduling and cancelling deadlines,
@SagaDeadlineHandler, durable delivery. -
Storage & tables —
@SagaTable, JSON-blob persistence, schema auto-creation, optimistic locking. -
Retention — deleting completed sagas after a maximum age.
-
Testing sagas — unit tests of a saga class, and integration tests that wait for a saga’s state.