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 |
|---|---|---|
|
PostgreSQL |
|
|
MySQL |
|
|
Microsoft SQL Server |
|
|
IBM DB2 |
|
|
Oracle Database |
|
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: