SagaEngine

open class SagaEngine<S : Saga<S>, T : SagaStep<T>>(sagaRepository: SagaRepositoryPort<S>, sagaStepRepository: SagaStepRepositoryPort<T>, objectMapper: ObjectMapper, entityFactory: SagaEntityFactory<S, T>, compensationRunner: SagaCompensationTrigger)(source)

Reusable saga participant engine for the choreography pattern.

The engine is built on top of persistence-agnostic ports (SagaRepositoryPort, SagaStepRepositoryPort) and collaborator hooks (SagaEntityFactory, SagaCompensator). The underlying storage can be JPA, MongoDB, DynamoDB, in-memory, etc. — the engine has no knowledge of it.

State transitions are performed exclusively through the immutable behavior methods exposed by Saga and SagaStep (DDD rich aggregate). The engine never mutates fields directly.

Choreography semantics (no central orchestrator):

  • startSaga — begins a new local saga

  • recordSagaStep — records a local step (does NOT auto-complete the saga)

  • awaitResponse — marks the saga as waiting for an external event

  • completeSaga — explicitly marks the saga as completed

  • failSaga — explicitly marks the saga as failed and triggers compensation

When a step is recorded with SagaStepStatus.FAILED, compensation is still triggered automatically because a local step failure is an immediate signal.

Constructors

Link copied to clipboard
constructor(sagaRepository: SagaRepositoryPort<S>, sagaStepRepository: SagaStepRepositoryPort<T>, objectMapper: ObjectMapper, entityFactory: SagaEntityFactory<S, T>, compensationRunner: SagaCompensationTrigger)

Functions

Link copied to clipboard
@Transactional
open fun awaitResponse(sagaId: String): S

Marks the saga as SagaStatus.AWAITING_RESPONSE — the request was dispatched and the orchestrator/participant is waiting for a reply. The SagaWatchdog will fail the saga if it stays in this status past SagaProperties.awaitResponseTimeout. No-op on terminal sagas.

Link copied to clipboard
@Transactional
open fun completeSaga(sagaId: String): S

Explicitly marks the saga as SagaStatus.COMPLETED. No-op (and returns the existing aggregate) if the saga is already in a terminal status.

Link copied to clipboard
@Transactional
open fun failSaga(sagaId: String, error: String): S

Transitions the saga to SagaStatus.COMPENSATING with the supplied error as the reason and schedules compensation after the current transaction commits. No-op on terminal sagas.

Link copied to clipboard
open fun findSagaById(sagaId: String): S?

Looks up the saga by id, returning null if absent.

Link copied to clipboard
@Transactional
open fun recordSagaStep(sagaId: String, stepName: SagaTypeValue, status: SagaStepStatus, payload: Any? = null): T

Type-safe overload of recordSagaStep that accepts the step name as a SagaTypeValue.

@Transactional
open fun recordSagaStep(sagaId: String, stepName: String, status: SagaStepStatus, payload: Any? = null): T

Records (or transitions) a saga step.

Link copied to clipboard
open fun runCompensation(sagaId: String)

Public entry point used by SagaWatchdog to retry compensation for sagas stuck in COMPENSATING or that ended in COMPENSATION_FAILED. Idempotent — safe to call multiple times.

Link copied to clipboard
@Transactional
open fun startSaga(sagaType: SagaTypeValue, payload: Any): S

Type-safe overload of startSaga that accepts the saga type as a SagaTypeValue so callers cannot pass an arbitrary string.

@Transactional
open fun startSaga(sagaType: String, payload: Any): S

Persists a new saga aggregate in SagaStatus.STARTED with a freshly generated UUID and a JSON-serialized payload. The saga's id is the correlation handle returned to the caller for subsequent recordSagaStep / completeSaga / failSaga calls.