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:

  1. Created — a @SagaStart event handler creates a new instance (or routes to an existing one if a duplicate arrives).

  2. Active — subsequent @SagaEventHandler events update the instance state; commands and deadlines may be issued from within these handlers.

  3. Completed — a @SagaEnd annotation on the terminal handler, or an explicit call to the injected SagaLifecycle.end(), tombstones the instance. Late re-deliveries of events to a completed saga are silently skipped — the instance is not resurrected.

Saga lifecycle: an event with @SagaStart creates a new instance; events route to it by association; @SagaEnd or SagaLifecycle.end() completes it; late events to a completed saga are skipped
Figure 1. A saga instance’s lifecycle

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.