Reactive Data Access

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.

This page covers Quarkus’s fully non-blocking data-access stack — the Vert.x reactive SQL clients, Hibernate Reactive, and their reactive NoSQL counterparts — as distinct from the blocking JDBC/Panache stack covered on Datasources and Transactions, Hibernate ORM in Quarkus, and Panache: The Quarkus Data API. Mutiny itself — the Uni/Multi reactive programming model used throughout this page — is covered in depth on the dedicated Mutiny page and is only used here, not re-explained.

Reactive SQL clients

The Vert.x Reactive SQL Client is a non-blocking database driver family: every call returns a Uni/Multi instead of blocking the calling thread, so a query can run to completion entirely on a Vert.x event loop thread. Quarkus wraps each database’s client in its own extension:

Extension Database quarkus.datasource.db-kind

quarkus-reactive-pg-client

PostgreSQL

postgresql

quarkus-reactive-mysql-client

MySQL

mysql

quarkus-reactive-mssql-client

Microsoft SQL Server

mssql

quarkus-reactive-db2-client

IBM DB2

db2

quarkus-reactive-oracle-client

Oracle Database

oracle

Extension setup and configuration

A Postgres-backed project adds the reactive client alongside the base quarkus-datasource extension:

./mvnw quarkus:add-extension -Dextensions=reactive-pg-client

which adds the following to pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>

application.properties configures the reactive datasource under quarkus.datasource.reactive., distinct from the blocking quarkus.datasource.jdbc. tree used by the JDBC stack:

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=book_app
quarkus.datasource.password=secret

quarkus.datasource.reactive.url=postgresql://localhost:5432/book_db
quarkus.datasource.reactive.max-size=20
quarkus.datasource.reactive.idle-timeout=PT10M

quarkus.datasource.reactive.url has no jdbc: prefix — it is a plain Vert.x connection URI, not a JDBC URL. quarkus.datasource.reactive.max-size bounds the connection pool; both the JDBC and reactive trees can be configured side by side on the same datasource if a project genuinely needs both access styles, though most projects pick one.

Injecting Pool and running queries

io.vertx.mutiny.sqlclient.Pool is the Mutiny-flavored, database-agnostic handle Quarkus injects; the database-specific io.vertx.mutiny.pgclient.PgPool (and its MySQL/MSSQL/DB2/Oracle equivalents) is the same pool typed to expose driver-specific extras:

package com.example.book;

import java.util.List;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.sqlclient.Pool;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.RowSet;
import io.vertx.mutiny.sqlclient.Tuple;

@ApplicationScoped
public class ReactiveBookRepository {

    private final Pool pool;

    public ReactiveBookRepository(Pool pool) {
        this.pool = pool;
    }

    public Uni<List<Book>> findByAuthor(String author) {
        return pool.preparedQuery("SELECT id, title, author FROM book WHERE author = $1")
                .execute(Tuple.of(author))
                .onItem().transform(this::toBooks);
    }

    public Uni<Book> findById(Long id) {
        return pool.preparedQuery("SELECT id, title, author FROM book WHERE id = $1")
                .execute(Tuple.of(id))
                .onItem().transform(RowSet::iterator)
                .onItem().transform(iterator -> iterator.hasNext() ? toBook(iterator.next()) : null);
    }

    private List<Book> toBooks(RowSet<Row> rows) {
        return rows.stream().map(this::toBook).collect(Collectors.toList());
    }

    private Book toBook(Row row) {
        return new Book(row.getLong("id"), row.getString("title"), row.getString("author"));
    }
}

preparedQuery(…​).execute(Tuple.of(…​)) returns a Uni<RowSet<Row>> that never blocks the subscribing thread; RowSet<Row> is iterated or streamed to map raw rows onto a DTO, exactly as ResultSet would be mapped in the blocking JDBC stack, but without ever occupying a thread while waiting on the database.

Transactions and connection pooling

pool.withTransaction(…​) runs a function against a transactional SqlConnection, committing if the returned Uni completes successfully and rolling back automatically if it fails:

package com.example.book;

import jakarta.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.sqlclient.Pool;
import io.vertx.mutiny.sqlclient.Tuple;

@ApplicationScoped
public class ReactiveBookTransferService {

    private final Pool pool;

    public ReactiveBookTransferService(Pool pool) {
        this.pool = pool;
    }

    public Uni<Void> transferOwnership(Long bookId, String fromUser, String toUser) {
        return pool.withTransaction(connection -> connection
                .preparedQuery("UPDATE book SET owner = $1 WHERE id = $2 AND owner = $3")
                .execute(Tuple.of(toUser, bookId, fromUser))
                .onItem().transformToUni(rows -> rows.rowCount() == 1
                        ? Uni.createFrom().voidItem()
                        : Uni.createFrom().failure(new IllegalStateException("Book " + bookId
                                + " is not currently owned by " + fromUser)))
                .onFailure().call(() -> connection.preparedQuery(
                        "INSERT INTO audit_log(book_id, event) VALUES ($1, 'transfer-failed')")
                        .execute(Tuple.of(bookId))));
    }
}

Nothing in this method blocks: the update, the failure branch, and the audit-log write all compose as Uni stages against the same transactional connection, so the whole transfer can run on an event loop thread without ever stalling it. quarkus.datasource.reactive.max-size caps how many concurrent connections the pool hands out to withTransaction/preparedQuery callers; exhausting it queues further requests rather than opening unbounded connections against the database.

Hibernate Reactive and Hibernate Reactive with Panache

Hibernate Reactive

quarkus-hibernate-reactive is the reactive counterpart to blocking Hibernate ORM (covered on Hibernate ORM in Quarkus): the same entity-mapping annotations (jakarta.persistence.*) apply, but every session operation is exposed through io.smallrye.mutiny.Mutiny’s `SessionFactory/Session, returning Uni<T> instead of a blocking value:

package com.example.book;

import jakarta.enterprise.context.ApplicationScoped;

import org.hibernate.reactive.mutiny.Mutiny;

import io.smallrye.mutiny.Uni;

@ApplicationScoped
public class ReactiveBookService {

    private final Mutiny.SessionFactory sessionFactory;

    public ReactiveBookService(Mutiny.SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Uni<BookEntity> findById(Long id) {
        return sessionFactory.withSession(session -> session.find(BookEntity.class, id));
    }

    public Uni<BookEntity> create(BookEntity book) {
        return sessionFactory.withTransaction((session, transaction) -> session.persist(book)
                .replaceWith(book));
    }
}

withSession opens (and closes) a Mutiny.Session for the duration of the Uni pipeline; withTransaction additionally wraps the operation in a database transaction, committing when the pipeline completes and rolling back on failure — the reactive equivalent of @Transactional on the blocking stack.

Hibernate Reactive with Panache

quarkus-hibernate-reactive-panache mirrors blocking Panache’s active-record style (covered on Panache: The Quarkus Data API), but every static finder/persist method returns Uni<T>/Uni<List<T>> instead of the blocking return types shown there:

package com.example.book;

import java.util.List;

import io.quarkus.hibernate.reactive.panache.PanacheEntity;

import jakarta.persistence.Column;

public class Book extends PanacheEntity {

    @Column(nullable = false)
    public String title;

    @Column(nullable = false)
    public String author;

    public static Uni<List<Book>> findByAuthor(String author) {
        return find("author", author).list();
    }
}

The side-by-side contrast below shows the same "find all books by author" operation on each stack:

// Blocking Panache (quarkus-hibernate-orm-panache) -- see xref:backend/quarkus/panache.adoc[]
public class BlockingBook extends PanacheEntity {
    public String author;

    public static List<BlockingBook> findByAuthor(String author) {
        return find("author", author).list(); // blocks the calling thread until the query completes
    }
}

// Reactive Panache (quarkus-hibernate-reactive-panache)
public class ReactiveBook extends PanacheEntity {
    public String author;

    public static Uni<List<ReactiveBook>> findByAuthor(String author) {
        return find("author", author).list(); // returns immediately with a Uni; never blocks
    }
}

The query DSL (find, list, Panache.withTransaction) reads almost identically to the blocking version — the difference that matters is entirely in the return type, and every caller further up the stack must be updated to compose on Uni rather than call the method as if it returned a plain value.

Reactive MongoDB and Redis clients

quarkus-mongodb-client exposes a reactive io.quarkus.mongodb.reactive.ReactiveMongoClient alongside its blocking MongoClient, and quarkus-redis-client exposes a reactive io.quarkus.redis.datasource.ReactiveRedisDataSource alongside its blocking RedisDataSource — both returning Uni/Multi in place of a blocking call, following the same pattern as the SQL clients above:

package com.example.book;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.mongodb.reactive.ReactiveMongoClient;
import io.quarkus.mongodb.reactive.ReactiveMongoCollection;
import io.smallrye.mutiny.Uni;
import org.bson.Document;

@ApplicationScoped
public class ReactiveBookCatalog {

    private final ReactiveMongoClient client;

    public ReactiveBookCatalog(ReactiveMongoClient client) {
        this.client = client;
    }

    public Uni<Document> findByIsbn(String isbn) {
        ReactiveMongoCollection<Document> books = client.getDatabase("catalog").getCollection("books");
        return books.find(new Document("isbn", isbn)).collect().first();
    }
}

Driver setup, connection configuration, and the blocking/general NoSQL story for both stores are covered on the NoSQL and other stores page and are not repeated here.

The blocking/reactive mixing rule

The hard rule underlying every API on this page: never call a blocking JDBC/Panache API from code running on the Vert.x event loop — a reactive resource method, or any stage of a Uni/Multi pipeline. Blocking that thread does not just slow down the current request; it stalls every other request currently assigned to the same event loop thread, since Quarkus runs only a small, fixed pool of them.

Detecting violations

Vert.x’s own BlockedThreadChecker watches every event loop thread and logs a WARN with a full stack trace once a thread has been occupied past a configurable threshold:

quarkus.vertx.max-event-loop-execute-time=2s

A WARN naming a thread as vert.x-eventloop-thread- pointing into JDBC, java.net.Socket, or any other blocking call is a direct signal that a blocking API was invoked somewhere it should not have been. The rule of thumb that avoids needing this checker in the first place: *if a method returns Uni/Multi, everything inside it must also be non-blocking, all the way down — one blocking call buried three layers into an otherwise reactive pipeline is enough to reintroduce the exact problem returning Uni was meant to avoid.

What to do instead

A genuinely blocking call that must run from otherwise-reactive code should be dispatched to a worker thread explicitly rather than mixed in directly — either by marking the whole method @Blocking (covered on the REST services page for resource methods), or, inside a pipeline that is only partly blocking, by hopping onto Mutiny’s default worker pool for just that stage:

package com.example.book;

import jakarta.enterprise.context.ApplicationScoped;

import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;

@ApplicationScoped
public class LegacyAuditBridge {

    public Uni<Void> recordLegacyAudit(Long bookId) {
        return Uni.createFrom().item(() -> {
                    LegacyAuditClient.blockingWrite(bookId); // blocking I/O, deliberately isolated
                    return null;
                })
                .runSubscriptionOn(Infrastructure.getDefaultWorkerPool())
                .replaceWithVoid();
    }
}

runSubscriptionOn(Infrastructure.getDefaultWorkerPool()) moves that single blocking stage onto a worker thread before it runs, keeping the surrounding pipeline’s event-loop stages untouched — the blocking call is isolated and explicit instead of silently stalling whichever event loop thread happened to be subscribing.

The diagram below contrasts the two request paths — a blocking JDBC call hopping to a worker thread and back before its event loop thread is free for the next request, against a reactive SQL client call that completes entirely on the event loop:

flowchart TD subgraph Blocking["Blocking JDBC/Panache path"] direction TB B1[Event loop receives request] --> B2[Plain-return-type dispatch\nhands off to worker thread] B2 --> B3[Worker thread makes\nblocking JDBC call] B3 --> B4[Worker thread blocks\nwaiting on the database] B4 --> B5[Result returned to worker thread] B5 --> B6[Response handed back\nto the event loop] B6 --> B7[Event loop free for\nthe next request] end subgraph Reactive["Reactive SQL client path"] direction TB R1[Event loop receives request] --> R2[Reactive SQL client call\nissued from the event loop] R2 --> R3[Event loop is free\nimmediately -- no blocking wait] R3 --> R4[Other requests processed\non the same event loop] R4 --> R5[Database completes the query,\nUni resolves via callback] R5 --> R6[Response written\nfrom the event loop] end