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

postgresql

quarkus-jdbc-postgresql

MySQL

mysql

quarkus-jdbc-mysql

MariaDB

mariadb

quarkus-jdbc-mariadb

Microsoft SQL Server

mssql

quarkus-jdbc-mssql

IBM DB2

db2

quarkus-jdbc-db2

Oracle

oracle

quarkus-jdbc-oracle

H2

h2

quarkus-jdbc-h2

Apache Derby

derby

quarkus-jdbc-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

REQUIRED (default)

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.

REQUIRES_NEW

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.

MANDATORY

Requires that a transaction already be active; throws a TransactionalException wrapping a jakarta.transaction.TransactionRequiredException if none is, making "must only ever be called from inside a transaction" an enforced contract rather than an assumption.

SUPPORTS

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.

NOT_SUPPORTED

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.

NEVER

Throws a TransactionalException if a transaction is already active; otherwise runs without one, enforcing the opposite contract from MANDATORY.

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:

flowchart TD A[REST resource method] --> B["Service.rename()\n@Transactional(REQUIRED)"] B --> C{Transaction already\nactive on this thread?} C -->|no| D[Begin new transaction] C -->|yes| E[Join existing transaction] D --> F[Repository.findById / update\nsame transaction] E --> F F --> G{Method returns\nnormally?} G -->|yes| H[Commit at the\nService.rename boundary] G -->|no, unchecked exception| I[Roll back at the\nService.rename boundary] B -.also calls.-> J["AuditLogRepository.record()\n@Transactional(REQUIRES_NEW)"] J --> K[Suspend caller's transaction] K --> L[Begin independent transaction] L --> M[Commit / roll back\nindependently] M --> N[Resume caller's\nsuspended transaction]