Transactors
A Transactor is how you run database operations. It obtains a connection, runs your code inside a transaction, and handles commit, rollback, and cleanup automatically.
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 — your IDE will autocomplete all available options. Pass the config to Transactor.create():
- Kotlin
- Java
- Scala
// 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())
// PostgreSQL
Transactor pgTx =
Transactor.create(
PgConfig.builder("localhost", 5432, "mydb", "user", "pass")
.sslmode(PgSslMode.REQUIRE)
.build());
// DuckDB (in-memory)
Transactor duckTx = Transactor.create(DuckDbConfig.inMemory().build());
// DuckDB (file-based)
Transactor duckFileTx =
Transactor.create(
DuckDbConfig.builder("/tmp/analytics.db").threads(4).memoryLimit("2GB").build());
// MariaDB / MySQL
Transactor mariaTx =
Transactor.create(MariaConfig.builder("localhost", 3306, "mydb", "user", "pass").build());
// Oracle
Transactor oracleTx =
Transactor.create(
OracleConfig.builder("localhost", 1521, "xe", "user", "pass")
.serviceName("XEPDB1")
.build());
// SQL Server — .encrypt(FALSE) is correct for local dev against the default container image
// (self-signed cert, no trust chain). Production should use .encrypt(TRUE) with a trusted
// certificate; the handshake failure against localhost is otherwise inscrutable.
Transactor mssqlTx =
Transactor.create(
SqlServerConfig.builder("localhost", 1433, "mydb", "user", "pass")
.encrypt(SqlServerEncrypt.FALSE)
.build());
// DB2
Transactor db2Tx =
Transactor.create(Db2Config.builder("localhost", 50000, "mydb", "user", "pass").build());
// 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 — .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:
var settings = ConnectionSettings.builder()
.transactionIsolation(TransactionIsolation.READ_COMMITTED)
.readOnly(true)
.schema("app")
.connectionInitSql("SET search_path TO app")
.build();
var tx = Transactor.create(config, settings);
// or: Transactor.create(config, settings, strategy)
| Setting | Description |
|---|---|
transactionIsolation | READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE |
autoCommit | Override the driver's default auto-commit mode |
readOnly | Hint to the driver that connections are read-only |
catalog | Set the default catalog |
schema | Set the default schema |
connectionInitSql | SQL 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
When a config requires single-connection mode (e.g. DuckDB in-memory), ConnectionSource.of() automatically reuses one connection across all callers — no special setup needed. DuckDB in-memory creates a separate database per connection, so this is detected and handled transparently:
// DuckDB in-memory: single-connection mode is automatic
var ds = ConnectionSource.of(DuckDbConfig.inMemory().build());
var tx = ds.transactor();
Strategies
The default strategy wraps each call in a transaction. Pass a different built-in strategy to Transactor.create():
| Strategy | Behavior |
|---|---|
defaultStrategy() | begin, commit on success, rollback on error, close |
autoCommitStrategy() | no transaction management, just close |
testStrategy() | begin, rollback on success or error, close — keeps test data isolated |
var tx = Transactor.create(config, Transactor.testStrategy());
testStrategy() rolls back every execute / transact, including ones that succeeded — nothing persists. If you copy this into a script, a tutorial, a migration, or any production code, your DDL and INSERTs will silently disappear. For anything that should persist, use defaultStrategy().
Strategies can be thoroughly customized with composable hooks for transaction lifecycle and observability. See Strategies for details.