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 CREATE SEQUENCE, so it is tempting to assume it behaves like PostgreSQL here. It does not. Making concurrent appends sound requires a database primitive MariaDB does not expose — a fresh view of the oldest still-in-flight transaction — so MariaDB uses the same serialized allocator as MySQL. This is a correctness decision, not an oversight.

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.

Embedded engines

H2 and HSQLDB are for embedded and test use only. They are not intended for production; use them for fast in-process tests and local development.