Skip to main content

Transactors

A Transactor runs database operations. It obtains a connection, runs your code inside a transaction, and handles commit, rollback, and cleanup.

By default, each call is wrapped in a transaction: auto-commit off, commit on success, rollback on error, close always.

Setting up

Each supported database has a typed config builder. Pass the config to Transactor.create():

// PostgreSQL
val pgTx =
Transactor.create(
PgConfig.builder(
"localhost", 5432, "mydb", "user", "pass")
.sslmode(PgSslMode.REQUIRE)
.build())

// DuckDB (in-memory)
val duckTx =
Transactor.create(DuckDbConfig.inMemory().build())

// DuckDB (file-based)
val duckFileTx =
Transactor.create(
DuckDbConfig.builder("/tmp/analytics.db")
.threads(4)
.memoryLimit("2GB")
.build())

// MariaDB / MySQL
val mariaTx =
Transactor.create(
MariaConfig.builder(
"localhost", 3306, "mydb", "user", "pass")
.build())

// Oracle
val oracleTx =
Transactor.create(
OracleConfig.builder(
"localhost", 1521, "xe", "user", "pass")
.serviceName("XEPDB1")
.build())

// SQL Server
// SQL Server — .encrypt(FALSE) for local dev (self-signed cert); use TRUE + trusted cert in prod
val mssqlTx =
Transactor.create(
SqlServerConfig.builder(
"localhost", 1433, "mydb", "user", "pass")
.encrypt(SqlServerEncrypt.FALSE)
.build())

// DB2
val db2Tx =
Transactor.create(
Db2Config.builder(
"localhost", 50000, "mydb", "user", "pass")
.build())

Connection settings

Override connection-level defaults by passing ConnectionSettings:

val settings = ConnectionSettings.builder()
.transactionIsolation(TransactionIsolation.READ_COMMITTED)
.readOnly(true)
.schema("app")
.connectionInitSql("SET search_path TO app")
.build()

val tx = Transactor.create(config, settings)
SettingDescription
transactionIsolationREAD_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
autoCommitOverride the driver's default auto-commit mode
readOnlyHint to the driver that connections are read-only
catalogSet the default catalog
schemaSet the default schema
connectionInitSqlSQL executed once when each connection is created

Connection pooling

For production, use HikariDataSourceFactory from the foundations-jdbc-hikari module:

var pool = HikariDataSourceFactory.create(config);
var tx = pool.transactor();

Single connection mode

SingleConnectionDataSource reuses one connection across all calls — needed for DuckDB in-memory, where each new connection creates a separate database:

var ds = SingleConnectionDataSource.create(config);
var tx = ds.transactor();

Test mode

Call .rollbackOnly() to roll back instead of committing — ideal for test isolation:

var tx = Transactor.create(config).rollbackOnly();

Observability

Attach a QueryListener to observe all queries and transactions:

var tx = Transactor.create(config).withListener(myListener);

See Observability for details.

Raw JDBC access

Transactor.create() returns TransactorJdbc — a subtype of Transactor that exposes the underlying JDBC connection:

TransactorJdbc tx = Transactor.create(config);

// Raw JDBC when you need it
tx.executeJdbc(conn -> {
var meta = conn.getMetaData();
return meta.getTables(null, null, "%", null);
});

This is an escape hatch for vendor-specific JDBC extensions, DatabaseMetaData, advisory locks, or migration tooling. executeJdbc is only available on TransactorJdbc.

Error handling

Database errors are thrown as DatabaseException — a sealed class with dialect-specific subtypes:

  • DatabaseException.Postgres — structured PostgreSQL error with all ErrorResponse fields (schema, table, constraint, position caret, etc.)
  • DatabaseException.SqlServer — structured SQL Server error with severity, procedure name, line number
  • DatabaseException.Jdbc — wraps SQLException for other databases

See Error Handling for pattern matching examples and field details.