Storage & tables
Spring DDD persists every saga instance as a JSON blob in a relational table.
The storage layer is accessed through a single SagaStore port, so you can swap or extend the persistence strategy without touching any saga class.
For a conceptual overview of the delivery model, see Overview. For writing saga classes, see Writing a saga.
The SagaStore SPI
SagaStore is the persistence port for saga instances, keyed by the pair (sagaType, sagaId):
interface SagaStore {
fun <T : Any> find(sagaType: String, sagaId: String, type: KClass<T>): LoadedSaga<T>?
fun save(sagaType: String, sagaId: String, instance: Any,
expectedVersion: Long?, completed: Boolean)
fun delete(sagaType: String, sagaId: String)
}
find returns null if no instance exists.
save is a dual-mode operation: when expectedVersion is null it performs a fresh insert;
when it is non-null it performs a version-guarded update.
delete is a no-op when the instance is absent.
JdbcJsonSagaStore: the built-in implementation
spring-ddd-starter-saga-jdbc wires JdbcJsonSagaStore, which serializes each saga instance to
a JSON blob using Jackson 3 (tools.jackson.databind.json.JsonMapper).
| Column | Description |
|---|---|
|
The saga type string (from |
|
The correlation value, reduced to a |
|
The full saga instance serialized as a JSON blob. |
|
A monotonically increasing counter used for optimistic locking.
New instances start at |
|
Completion timestamp; set when the instance is tombstoned via |
Optimistic locking
Two concurrent threads that both attempt to start a saga for the same (sagaType, sagaId) pair
will both try to insert a row at version = 0.
The one that loses the primary-key race receives an OptimisticLockingFailureException.
All subsequent saves use a version-guarded UPDATE:
UPDATE {table}
SET state_json = :stateJson,
completed_at = :completedAt,
version = version + 1
WHERE saga_type = :sagaType
AND saga_id = :sagaId
AND version = :expectedVersion
If the UPDATE touches zero rows — because another thread already incremented the version — the
store raises OptimisticLockingFailureException.
The saga engine then retries the full load-handle-save cycle.
Routing saga instances to tables with @SagaTable
By default all saga instances share a single table.
Add @SagaTable (from spring-ddd-saga-jdbc) to a saga class to route its instances to a
dedicated table.
| Declaration | Resolved table |
|---|---|
(absent) |
|
|
Simple class name in |
|
|
Multiple saga types may share a table (for example to group them by bounded context).
The saga_type column always disambiguates rows within a shared table.
@SagaTable must be declared directly on the saga class; it is not inherited.
Placing it on a class that is not annotated with @Saga is a startup configuration error.
Banking example
A funds-transfer saga can route its instances to a dedicated table in two ways:
@Saga(type = "transfer-saga")
@SagaTable (1)
class TransferSaga { /* … */ }
| 1 | Blank value — resolves to TRANSFER_SAGA. |
@Saga(type = "transfer-saga")
@SagaTable("FUNDS_TRANSFER") (1)
class TransferSaga { /* … */ }
| 1 | Explicit name — resolves to FUNDS_TRANSFER. |
Automatic table creation
SagaTableInitializer applies the instance-table DDL at startup when
spring.ddd.saga.schema.auto is true (the default).
It creates every table registered in the SagaTableRegistry — the shared default table plus
every @SagaTable target — using per-dialect CREATE TABLE templates.
Spring DDD ships templates for six dialects: Postgres, MySQL, MariaDB, H2, HSQLDB, and SQL Server.
To own the DDL yourself (Flyway, Liquibase, or manual migration), set:
spring.ddd.saga.schema.auto=false
Separate flags for deadline and inbound checkpoint tables
The deadline tables (SAGA_DEADLINE and SAGA_DEADLINE_LOCK) and the ordered-inbound checkpoint
table (SAGA_INBOUND_CHECKPOINT) have their own independent flags:
| Property | Default | Covers |
|---|---|---|
|
|
Instance tables (shared |
|
|
|
|
|
|
Setting one flag to false does not affect the others.
Use this to hand off deadline or inbound tables to your migration tool while still letting Spring
DDD create the instance tables, or vice versa.
For the per-dialect DDL, see Database table schemas. For the full list of configurable table names and related properties, see Configuration properties.
Related pages
-
Overview — concept, delivery model, starters.
-
Deadlines & timeouts — scheduling named deadlines; durable deadline tables.
-
Retention — sweeping completed sagas once
completed_atis old enough. -
Ordered event delivery — opt-in ordered inbound and the
SAGA_INBOUND_CHECKPOINTtable.