Choosing a database
Spring DDD runs on six databases. All of them are correct — no supported engine loses or reorders events — but they differ in one operational dimension: how the global event position is assigned, which sets the ceiling on sustained append throughput. (The global position is a single application-wide counter shared across all event tables — see Per-aggregate event tables for why it exists.) This page helps you pick on those merits. Engine choice does not change how you write aggregates, projections, or sagas; that code is identical on every engine.
Append concurrency by engine
| Engine | Append concurrency | Global-position mechanism | Suitable for |
|---|---|---|---|
PostgreSQL |
Concurrent (high write throughput) |
Shared database sequence |
Production, write-heavy |
SQL Server |
Concurrent (high write throughput) |
Shared database sequence |
Production, write-heavy |
MySQL |
Serialized — appends cluster-wide take one lock |
Shared allocator counter |
Production; lower sustained append throughput |
MariaDB |
Serialized — appends cluster-wide take one lock |
Shared allocator counter |
Production; lower sustained append throughput |
H2 / HSQLDB |
n/a |
— |
Embedded / testing only |
What "serialized appends" means
On PostgreSQL and SQL Server, global_position is drawn from a native database sequence, so many
application threads — across many instances — can append events at the same time; the database hands
out positions concurrently.
On MySQL and MariaDB, global_position comes from a single allocator row rather than a shared sequence
(MySQL has none the framework can use; MariaDB has one but cannot use it soundly — see the note below).
Reserving the next position takes a row lock held until the appending transaction commits, which means
appends across the whole cluster are serialized — one at a time.
Correctness is unaffected — this is in fact why these engines never produce a position gap — but
sustained append throughput is capped by that single lock.
For a write-heavy workload on MySQL or MariaDB, expect the append rate to become the bottleneck well
before read or projection throughput does.
|
MariaDB has a native |
What engine choice does not change
-
Read and projection semantics are identical on every engine. Catch-up, replay, high-water-mark tracking, and projection delivery behave the same regardless of the database.
-
No engine loses or reorders events. Every production engine is correct under concurrent appends and multiple instances. The serialized engines trade throughput for their gap-free guarantee; the concurrent engines resolve position gaps soundly at read time. Either way, nothing at or below the read frontier is ever skipped.
-
There is nothing to tune. The read frontier is derived from the database’s own transaction visibility, not a timeout — see Running on multiple instances.
SQL Server: custom event tables need a rowversion column
If you define your own event table with @EventTable on SQL Server, it must include a
row_version ROWVERSION NOT NULL column.
The frontier reads it to distinguish a committed append from an in-flight one; a custom table that omits
it silently breaks catch-up for that table.
The column is auto-generated — never named in an INSERT — so adding it is a DDL-only change.
No other engine needs it.
See Database table schemas for the full DDL.