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.
The built-in JDBC store
spring-ddd-starter-saga-jdbc provides a SagaStore that stores each saga instance as JSON, written with Jackson 3 (tools.jackson.databind.json.JsonMapper).
Each instance is stored with its saga type and id, which together identify it, its state as JSON, a version, and the time it ended, empty while it is active. A new instance starts at a random version, so an instance started again under the id of a deleted one never repeats a version the earlier one held, and a save based on the earlier instance fails instead of overwriting the new one. The time it ended drives retention. See Database table schemas for the columns.
Optimistic locking
Two threads that start the same saga instance at the same time both try to create it; the one that loses fails with OptimisticLockingFailureException.
Every later save checks the version it loaded, so a save that another save overtook fails the same way, and nothing it changed is stored.
Nothing retries it in place: the event is handled again the way any failed handler’s event is.
The saga consumer retries it; see Event delivery.
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) |
The shared instance table, |
|
|
|
|
A name written on @SagaTable is used as written, without the prefix.
Multiple saga types may share a table (for example to group them by bounded context). The saga type keeps instances of different saga types apart in 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.
A table name is a physical SQL identifier and must match ^[A-Za-z_][A-Za-z0-9_]{0,62}$.
The limit applies to derived names too: <NAME>_INSTANCES adds ten characters to the class name and the table prefix goes before it, so a long saga class name fails at startup.
Set an explicit @SagaTable value to shorten it.
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_INSTANCES. |
@Saga(type = "transfer-saga")
@SagaTable("FUNDS_TRANSFER") (1)
class TransferSaga { /* … */ }
| 1 | Explicit name — resolves to FUNDS_TRANSFER. |
Table names and creation
All saga tables are set in one group:
spring.ddd.saga.jdbc:
initialize-schema: always # always | embedded | never
table-prefix: "" # e.g. ACME_
table-names:
saga-instances: SAGA_INSTANCES
saga-deadlines: SAGA_DEADLINES
saga-deadline-locks: SAGA_DEADLINE_LOCKS
initialize-schema decides whether the saga tables are created at startup: the shared instance table, every @SagaTable target, and the deadline and deadline-lock tables; the lock table is created with the deadline table unless the application supplies its own deadline lease.
always-
Creates them. The default.
embedded-
Creates them only in an embedded database, such as H2 in tests, while production applies its own migrations.
never-
Creates nothing. Apply the DDL yourself, with Flyway, Liquibase or a manual migration.
table-prefix goes before every saga table name, a name derived by @SagaTable included, so two applications can keep their saga tables apart in one schema.
It does not go before a name written on @SagaTable: two applications sharing a schema must give such a saga a different explicit name each, or they share one table.
Spring DDD ships templates for six dialects: PostgreSQL, MySQL, MariaDB, H2, HSQLDB, and SQL Server.
For the per-dialect DDL, see Database table schemas. For every saga property, see Configuration properties.
Related pages
-
Overview — concept, delivery model, starters.
-
Deadlines & timeouts — scheduling named deadlines; durable deadline tables.
-
Retention — deleting completed sagas once they ended long enough ago.
-
Event delivery — the topics a saga reads and its consumer.
-
Several DataSources — which
DataSourceand transaction manager the saga stores use.