Datasources and Transactions
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Every Quarkus service that talks to a database sits on top of two independent pieces: Agroal, the connection pool and datasource layer, and Narayana, the JTA transaction manager that coordinates commits and rollbacks around the work a request performs. This page covers configuring a blocking JDBC or reactive datasource, tuning and naming multiple datasources, declarative and programmatic transaction control with Narayana, and where XA two-phase commit and its alternatives (LRA, and Software Transactional Memory as a contrasting concept) fit in. Object-relational mapping itself — Hibernate ORM and Panache — is deliberately not repeated here; see the Hibernate ORM and JPA page and the Panache page for that.
Configuring a datasource
JDBC vs. reactive datasources
A blocking JDBC datasource is backed by a quarkus-jdbc- driver extension and the Agroal connection pool
(quarkus-agroal, pulled in transitively); a *reactive datasource is backed by a quarkus-reactive--client
extension instead, using Vert.x’s own non-blocking client and connection pool. Adding either kind of extension
also brings in quarkus-datasource, which supplies the common quarkus.datasource. configuration namespace
both share:
# blocking JDBC, PostgreSQL
./mvnw quarkus:add-extension -Dextensions=jdbc-postgresql
# reactive, PostgreSQL
./mvnw quarkus:add-extension -Dextensions=reactive-pg-client
JDBC configuration:
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=app
quarkus.datasource.password=app
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/app
package com.example.book;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.sql.DataSource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class BookCountService {
@Inject
DataSource dataSource;
public long countBooks() throws SQLException {
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) FROM book");
ResultSet resultSet = statement.executeQuery()) {
resultSet.next();
return resultSet.getLong(1);
}
}
}
Reactive configuration uses a single connection URI instead of separate username/password/JDBC-URL properties
(credentials can still be split out via quarkus.datasource.username/password if the URI omits them):
quarkus.datasource.reactive.url=postgresql://localhost:5432/app
quarkus.datasource.username=app
quarkus.datasource.password=app
package com.example.book;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.pgclient.PgPool;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class ReactiveBookCountService {
@Inject
PgPool client;
public Uni<Long> countBooks() {
return client.query("SELECT COUNT(*) FROM book")
.execute()
.onItem().transform(rows -> rows.iterator().next().getLong(0));
}
}
A blocking JDBC datasource is what a @Transactional/Hibernate ORM-based service normally wants (see
Transactions with Narayana JTA and the Hibernate ORM and
JPA page); a reactive datasource is for code that already runs non-blocking end to end, as covered on
the Quarkus REST page's @Blocking/non-blocking dispatch section. The
two are not mutually exclusive — both extensions can be added to the same project against the same underlying
database when different parts of the codebase need different execution models.
Supported database kinds
| Database | db-kind |
JDBC extension |
|---|---|---|
PostgreSQL |
|
|
MySQL |
|
|
MariaDB |
|
|
Microsoft SQL Server |
|
|
IBM DB2 |
|
|
Oracle |
|
|
H2 |
|
|
Apache Derby |
|
|
Reactive clients exist only for a subset of these — PostgreSQL, MySQL, MSSQL, DB2, and Oracle
(quarkus-reactive-pg-client, quarkus-reactive-mysql-client, quarkus-reactive-mssql-client,
quarkus-reactive-db2-client, quarkus-reactive-oracle-client); H2 and Derby are JDBC-only, which is one
reason both remain common choices for tests and local development regardless of which driver production uses.
Connection pool tuning with Agroal
Every JDBC datasource’s pool is Agroal, tuned under the quarkus.datasource.jdbc.* prefix:
quarkus.datasource.jdbc.min-size=5
quarkus.datasource.jdbc.max-size=20
quarkus.datasource.jdbc.acquisition-timeout=5S
quarkus.datasource.jdbc.leak-detection-interval=10S
quarkus.datasource.jdbc.idle-removal-interval=5M
quarkus.datasource.jdbc.max-lifetime=30M
min-size/max-size bound how many pooled connections Agroal keeps open; acquisition-timeout is how long a
thread waits for a connection before failing rather than blocking indefinitely; leak-detection-interval logs
a warning naming the stack trace of any connection held longer than that duration without being returned to the
pool, which is invaluable for finding a try-with-resources block that was forgotten somewhere; idle-removal-
interval and max-lifetime retire idle or aged-out connections so the pool does not hold onto connections a
firewall or database server has silently dropped.
Multiple and named datasources
A service touching more than one database declares each as a named datasource under
quarkus.datasource."<name>".*, and injects each by name with the io.quarkus.agroal.DataSource qualifier:
quarkus.datasource."orders".db-kind=postgresql
quarkus.datasource."orders".jdbc.url=jdbc:postgresql://localhost:5432/orders
quarkus.datasource."orders".username=orders
quarkus.datasource."orders".password=orders
quarkus.datasource."audit".db-kind=postgresql
quarkus.datasource."audit".jdbc.url=jdbc:postgresql://localhost:5432/audit
quarkus.datasource."audit".username=audit
quarkus.datasource."audit".password=audit
package com.example.book;
import java.sql.Connection;
import java.sql.SQLException;
import io.quarkus.agroal.DataSource;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class MultiDatasourceService {
@Inject
@DataSource("orders")
javax.sql.DataSource ordersDataSource;
@Inject
@DataSource("audit")
javax.sql.DataSource auditDataSource;
public Connection ordersConnection() throws SQLException {
return ordersDataSource.getConnection();
}
}
The unnamed, "default" datasource (plain quarkus.datasource., injected with no @DataSource qualifier at
all) and any number of named ones can coexist; each is its own independent Agroal pool with its own pool-tuning
properties under its own quarkus.datasource."<name>".jdbc. prefix.
Datasource health checks
Adding quarkus-smallrye-health to a project that already has a datasource configured registers a readiness
check for it automatically — no code required — visible at /q/health/ready alongside every other extension’s
own health contributions:
./mvnw quarkus:add-extension -Dextensions=smallrye-health
The check is enabled by default and can be turned off, per datasource, with quarkus.datasource.health.enabled
(or quarkus.datasource."<name>".health.enabled for a named one):
quarkus.datasource.health.enabled=false
Dev Services for databases
When no quarkus.datasource.jdbc.url (or quarkus.datasource.reactive.url) is set and a matching JDBC or
reactive driver extension is on the classpath, Dev Services auto-starts a matching container — Testcontainers
under the hood — the moment quarkus:dev or ./mvnw test runs, with zero configuration: no local database
install, no docker-compose file, no connection string to copy around. It is torn down again when the dev-mode
session or test run ends, unless configured to persist across runs:
quarkus.datasource.devservices.image-name=postgres:16-alpine
quarkus.datasource.devservices.reuse=true
image-name pins a specific container image instead of Quarkus’s own default for the configured db-kind;
reuse=true asks Testcontainers to keep the container running across separate dev-mode/test invocations rather
than starting a fresh one each time (Testcontainers' own reuse support must also be enabled locally, via
testcontainers.reuse.enable=true in ~/.testcontainers.properties, for this to take effect). Dev Services
switches itself off automatically — with no extra property needed — as soon as quarkus.datasource.jdbc.url
or quarkus.datasource.reactive.url is configured explicitly in any active profile; it can also be disabled
outright with quarkus.datasource.devservices.enabled=false, useful when a project’s own docker-compose setup
or a shared development database should be used instead.
Transactions with Narayana JTA
Narayana is the JTA (Jakarta Transactions) implementation Quarkus embeds; every @Transactional method,
every QuarkusTransaction call, and every injected UserTransaction ultimately drives the same Narayana
transaction manager underneath.
@Transactional and rollback rules
jakarta.transaction.Transactional on a CDI bean method wraps its execution in a transaction, chosen and
committed or rolled back according to that method’s TxType (see Propagation with TxType below):
package com.example.book;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class BookService {
@Inject
BookRepository bookRepository;
@Transactional(rollbackOn = IllegalArgumentException.class, dontRollbackOn = BookNotFoundException.class)
public Book rename(Long id, String newTitle) {
Book book = bookRepository.findById(id);
if (book == null) {
throw new BookNotFoundException(id);
}
if (newTitle.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}
book.title = newTitle;
return book;
}
}
By default a transaction rolls back on any unchecked exception (RuntimeException or Error) and commits
through any checked exception; rollbackOn/dontRollbackOn override that default per exception type, as
above, where a validation failure should abort the transaction but a "not found" outcome should not.
Propagation with TxType
TxType |
Behavior |
|---|---|
|
Joins the caller’s transaction if one is already active on the thread; otherwise begins a new one. The right default for almost every service method. |
|
Suspends any transaction already active on the calling thread, runs in a brand-new, independent transaction for the duration of the method, then resumes the caller’s suspended transaction afterward. Use it when a piece of work (an audit record, a notification) must commit — or roll back — on its own, regardless of what the caller ultimately does. |
|
Requires that a transaction already be active; throws a |
|
Joins the caller’s transaction if one is active; otherwise runs with no transaction at all. Rarely the right choice for anything that writes data, since whether writes are transactional then depends entirely on the caller. |
|
Suspends any transaction already active, runs the method with no transaction, then resumes the caller’s transaction afterward — for work that must not participate in the surrounding transaction at all, such as a diagnostic query against a long-running scan. |
|
Throws a |
Programmatic control: QuarkusTransaction and UserTransaction
io.quarkus.narayana.jta.QuarkusTransaction is Quarkus’s own, modern API for programmatic transaction control — a fluent builder, no checked exceptions to catch — and is the recommended way to reach for a transaction
outside of a plain @Transactional method boundary:
package com.example.book;
import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class AuditingBookService {
@Inject
BookRepository bookRepository;
@Inject
AuditLogRepository auditLogRepository;
public Book rename(Long id, String newTitle) {
Book book = QuarkusTransaction.requiringNew().call(() -> {
Book found = bookRepository.findById(id);
found.title = newTitle;
return found;
});
// Runs in its own transaction, independent of (and after) the one above.
QuarkusTransaction.requiringNew()
.run(() -> auditLogRepository.record(id, "renamed to " + newTitle));
return book;
}
}
requiringNew() mirrors TxType.REQUIRES_NEW; QuarkusTransaction also exposes joiningExisting() (mirrors
REQUIRED) and disallowingExisting() (mirrors NEVER), each with .call(…)/.run(…) for a whole unit
of work, or explicit .begin()/.commit()/.rollback() for imperative control:
package com.example.book;
import java.util.List;
import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class ManualTransactionBookService {
@Inject
BookRepository bookRepository;
public void archiveAll(List<Long> ids) {
QuarkusTransaction.begin();
try {
for (Long id : ids) {
bookRepository.archive(id);
}
QuarkusTransaction.commit();
} catch (RuntimeException e) {
QuarkusTransaction.rollback();
throw e;
}
}
}
jakarta.transaction.UserTransaction is the standard, portable Jakarta Transactions equivalent — the API to
reach for when the code must stay portable to another Jakarta EE-compliant runtime, at the cost of checked
exceptions on every call:
package com.example.book;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.NotSupportedException;
import jakarta.transaction.RollbackException;
import jakarta.transaction.SystemException;
import jakarta.transaction.UserTransaction;
@ApplicationScoped
public class PortableBookService {
@Inject
UserTransaction userTransaction;
@Inject
BookRepository bookRepository;
public void archiveAll(List<Long> ids) throws NotSupportedException, SystemException, RollbackException {
userTransaction.begin();
try {
for (Long id : ids) {
bookRepository.archive(id);
}
userTransaction.commit();
} catch (RuntimeException e) {
userTransaction.rollback();
throw e;
}
}
}
For new Quarkus code with no cross-runtime portability requirement, QuarkusTransaction is the better default:
unchecked, and its builder makes propagation and timeout explicit at the call site rather than implicit in
which method got called.
Timeouts
A default transaction timeout applies to every transaction unless overridden:
quarkus.transaction-manager.default-transaction-timeout=60S
A single call can override it via `QuarkusTransaction’s builder, without changing the default for anything else:
package com.example.book;
import java.util.List;
import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class TimeBoundBookService {
@Inject
BookRepository bookRepository;
public void bulkImport(List<Book> books) {
QuarkusTransaction.requiringNew()
.timeout(30)
.run(() -> books.forEach(bookRepository::persist));
}
}
A transaction that outlives its timeout is rolled back by Narayana’s reaper thread on its own, independent of
whatever the application code is doing at that moment — the next operation attempted against it fails with a
jakarta.transaction.RollbackException (or an equivalent unchecked failure under QuarkusTransaction) rather
than being allowed to commit.
Advanced: injecting TransactionManager directly
jakarta.transaction.TransactionManager gives direct access to the active Transaction object — suspending
and resuming it manually, or registering a Synchronization callback invoked just before and just after
completion — for framework-level code that needs a hook @Transactional/QuarkusTransaction/UserTransaction
do not expose. Application code should reach for it only when those higher-level APIs genuinely cannot express
what is needed:
package com.example.book;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.RollbackException;
import jakarta.transaction.Status;
import jakarta.transaction.Synchronization;
import jakarta.transaction.SystemException;
import jakarta.transaction.TransactionManager;
@ApplicationScoped
public class TransactionSynchronizationService {
@Inject
TransactionManager transactionManager;
public void registerCacheInvalidation(Runnable afterCommit) throws SystemException, RollbackException {
transactionManager.getTransaction().registerSynchronization(new Synchronization() {
@Override
public void beforeCompletion() {
// no-op: nothing needs to run before the transaction attempts to commit
}
@Override
public void afterCompletion(int status) {
if (status == Status.STATUS_COMMITTED) {
afterCommit.run();
}
}
});
}
}
XA transactions, sagas, and alternatives
When XA is actually needed
XA is a two-phase commit protocol that lets a single JTA transaction span two genuinely distinct transactional resources — two separate databases, or a database and a JMS broker — so that both commit or both roll back together, with no window in which one succeeded and the other did not. Enabling it for a JDBC datasource is a matter of an XA-capable driver plus one property:
quarkus.datasource.jdbc.transactions=xa
That guarantee is not free. XA needs a driver that actually implements javax.sql.XADataSource (not every
driver, or every managed database offering, supports it equally well); the two-phase protocol itself adds a
round trip and holds locks on both resources for longer than a single-resource commit would; and it couples the
availability of each participant to the other’s — if one resource is down when the coordinator tries to
complete the second phase, the transaction is left in doubt until Narayana’s persisted transaction log lets it
recover once that resource comes back. A plain single-resource JTA transaction — the default, no xa property
needed — has none of this cost, because there is only one resource for the transaction manager to coordinate in
the first place. The practical rule: reach for XA only when a single unit of work genuinely must commit
atomically across two separate transactional resources; a service that only ever touches one database, which is
the large majority of them, should stay on the default and never turn XA on.
Sagas with Narayana LRA
XA’s lockstep, hold-both-resources-until-both-agree model does not scale to a saga that spans multiple independent services over a potentially long duration — each service would have to expose an XA-compatible resource and stay locked for as long as the whole saga takes. Narayana LRA (Long Running Actions) is designed for exactly that case instead: each participating service completes and commits its own local work immediately, registers a compensating action up front, and if the overall saga later fails, the LRA coordinator calls each already-completed participant’s compensating action to undo its effect — rather than holding a single distributed transaction open across every service for the saga’s entire lifetime. For the domain-model side of sagas — local transactions and compensating actions as an aggregate concern, independent of the LRA transport — see Sagas.
Software Transactional Memory: a different model, not a Quarkus feature
Software Transactional Memory (STM) is a general concurrency-control concept worth naming precisely because it
is easy to confuse with JTA: STM is optimistic and in-memory, letting concurrent transactions proceed against
shared state and retrying one automatically when a conflicting commit is detected, with no locks held while the
transaction runs. JTA, by contrast, is pessimistic and resource-manager-coordinated: a @Transactional method
holds locks in the underlying database (or other resource) for the duration of the transaction, and conflicting
work blocks rather than retries. Quarkus does not ship a first-party STM extension, so this is a conceptual
contrast to keep in mind when reasoning about concurrency, not a mechanism configured anywhere on this page.
Isolation levels and locking
SQL isolation levels and row-/table-level locking are a distinct topic from transaction propagation and are not repeated here; see the isolation and locking page for that theory, which applies to a Quarkus datasource exactly as it does to any other JDBC connection.
Transaction boundaries across a call chain
The diagram below traces a REST resource calling a @Transactional(REQUIRED) service method, which in turn
calls a repository — the transaction begun at the service boundary is what is propagated down to the
repository call and committed or rolled back at that same boundary — alongside a second, independent call
made with REQUIRES_NEW, which suspends the caller’s transaction for its own duration: