Streaming Database Rows in Real-Time with Quarkus, Mutiny, and Hibernate Reactive
Learn how to stream PostgreSQL rows to your browser.
Modern applications don’t always fit the traditional request-response model. Whether you're powering dashboards, monitoring systems, IoT backends, or live feeds, sometimes you need a continuous stream of data instead of waiting for full responses. That’s where reactive programming comes in. With Quarkus, you can make it feel easy.
In this article, we’ll build a streaming web application using Quarkus, Hibernate Reactive, SmallRye Mutiny, and PostgreSQL. You’ll learn how to stream rows from a database as they are queried, delivering them to the frontend in real-time using Server-Sent Events (SSE).
Let’s take a further look into what makes this possible before jumping into the code.
Why Streaming Matters for Java Developers
Streaming is ideal when you have large datasets or frequent updates and don't want to load everything upfront. Imagine:
Real-time stock tickers
Chat messages
System metrics
Event logs
Sensor data from IoT devices
Slowly evolving datasets like paginated reports
In traditional Java EE or Spring apps, you’d need to buffer all your data, wrap it in a response object, and send it all at once. But this doesn’t scale well for large result sets or interactive UIs. With Quarkus and its reactive stack, you can stream data out of your database as it arrives, without blocking threads.
Behind the Scenes: Mutiny, SmallRye Reactive, and Hibernate Reactive
Quarkus uses Mutiny as its core reactive programming library. It’s based on the Reactive Streams spec but offers a developer-friendly API that avoids the complexity of traditional reactive libraries like RxJava or Reactor.
Mutiny introduces two core types:
Uni<T>
: represents a single result (think of it like aFuture<T>
)Multi<T>
: represents a stream of results (think of it like aPublisher<T>
)
In this tutorial, we’ll use Multi<Fruit>
to stream our data.
The database access layer is powered by Hibernate Reactive, which uses Vert.x SQL clients under the hood. Unlike standard Hibernate, it never blocks on I/O. Instead, it integrates with Mutiny to provide non-blocking access to relational databases like PostgreSQL.
You can read more about Reactive in my recent article:
Buzzers Over Blocking: Reactive Java Explained with Burgers and Mutiny
Traditional Java programming models block threads like restaurant customers frozen at the counter, waiting for their meal. In contrast, Quarkus with Mutiny embraces reactive programming while freeing up resources with non-blocking, event-driven flows. This article explains it all with a restaurant metaphor and live code examples to help you internalize …
Let’s Build a Real-Time Fruit Streamer
We’re going to build a web app that streams rows from a fruit
table in real-time and displays them on a browser using Server-Sent Events (SSE).
Create the Quarkus Project
Start by generating the project using your terminal and the following Maven command. This command includes all the necessary extensions.
If you want to see a working example, make sure to check out the Github repository.
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=reactive-streaming-example \
-Dextensions="rest-jackson,hibernate-reactive-panache,reactive-pg-client"
cd reactive-streaming-example
This creates a new Quarkus project in a directory named reactive-streaming-example
.
Configure the Database
Next, configure how Hibernate is handling the table creation. Add the following to your src/main/resources/application.properties
file.
# Hibernate Reactive Configuration
quarkus.hibernate-orm.database.generation=drop-and-create
Add Data
To effectively demonstrate the stream, you need a good amount of data. Create a file named import.sql
in the src/main/resources/
directory and add the following SQL statements. This file will automatically populate the database when the application starts.
src/main/resources/import.sql
CREATE SEQUENCE hibernate_sequence START 1 INCREMENT 1;
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Apple');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Banana');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Cherry');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Orange');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Pear');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Strawberry');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Blueberry');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Raspberry');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Grape');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Kiwi');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Mango');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Pineapple');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Watermelon');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Cantaloupe');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Honeydew');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Peach');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Plum');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Nectarine');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Apricot');
INSERT INTO fruit(id, name) VALUES (nextval('hibernate_sequence'), 'Pomegranate');
Define the Entity
Now, create the Fruit
entity. This is a simple Panache entity that maps to the fruit
table.
Create src/main/java/org/acme/Fruit.java
package org.acme;
import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Fruit extends PanacheEntity {
public String name;
}
Create the Streaming Endpoint
This JAX-RS resource streams all Fruit
entities directly from the database as server-sent events. The data will flow as fast as Hibernate Reactive can retrieve and send it. Rename the scaffolded GreetingResource.
src/main/java/org/acme/FruitResource.java
package org.acme;
import org.jboss.resteasy.reactive.RestStreamElementType;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.smallrye.mutiny.Multi;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/fruits")
public class FruitResource {
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<Fruit> streamFruits() {
return Panache.withSession(() -> Fruit.listAll())
.onItem().transformToMulti(Multi.createFrom()::iterable)
.onItem().castTo(Fruit.class);
}
}
Some specialties:
@Produces(MediaType.SERVER_SENT_EVENTS)
: This annotation tells the browser to keep the connection open to receive a stream of events.@RestStreamElementType(MediaType.APPLICATION_JSON)
: This specifies that each individual item in the stream is a JSON object.Step 1: Panache.withSession(() -> Fruit.listAll())
Executes the database query within a reactive session context
Fruit.listAll() returns Uni<List<Fruit>> - a single future containing all fruits
The session ensures proper database connection management
Step 2: .onItem().transformToMulti(Multi.createFrom()::iterable)
Converts Uni<List<Fruit>> to Multi<Fruit>
Takes the single list and transforms it into a stream of individual fruit items
Each fruit becomes a separate event in the stream
Step 3: .onItem().castTo(Fruit.class)
Ensures proper type casting (handles generic type erasure issues)
Guarantees each streamed item is properly typed as Fruit
Build the Frontend
Create a simple HTML file to receive and display the streamed data. Place this file in the src/main/resources/META-INF/resources
directory so Quarkus can serve it automatically.
src/main/resources/META-INF/resources/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fruit Stream 🍇</title>
<style>
body { font-family: sans-serif; background-color: #f4f4f9; color: #333; }
h1 { color: #5a2d82; }
#fruit-list { list-style: none; padding: 0; }
#fruit-list li { background: #fff; margin: 5px 0; padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
</style>
</head>
<body>
<h1>Streaming Fruits</h1>
<ul id="fruit-list"></ul>
<script>
const fruitList = document.getElementById('fruit-list');
const eventSource = new EventSource('/fruits');
eventSource.onmessage = function(event) {
const fruit = JSON.parse(event.data);
const listItem = document.createElement('li');
listItem.textContent = `ID: ${fruit.id} - Name: ${fruit.name}`;
fruitList.appendChild(listItem);
console.log('Received fruit:', fruit);
};
eventSource.onerror = function(event) {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Stream completed successfully');
} else {
console.log('Stream error:', event);
}
};
</script>
</body>
</html>
The key component here is the EventSource('/fruits')
JavaScript object, which connects to our streaming endpoint and listens for new data.
Run the Application
You are now ready to see it in action. Run the application in dev mode from your terminal:
./mvnw quarkus:dev
Open your browser and navigate to http://localhost:8080
You will see the list of fruits populate on the page very quickly as they are streamed from the backend.
Wrapping Up
This tiny app shows how easy it is to wire together Quarkus, Hibernate Reactive, Mutiny, and SSE for efficient database streaming. It’s the tip of the iceberg.
Imagine replacing the fruit stream with:
Sensor readings from edge devices
Event logs from microservices
Order processing pipelines
Live analytics dashboards
With Quarkus’s reactive stack, you're building on solid foundations. It’s fast. It’s resource-efficient. And it’s designed for the future of cloud-native Java.
Stay reactive. Stay curious.
Hi I Markus, thanks for these tutorials, I'm learning a lot Quarkus with them.
I was unable to run the example as it is:
First, the The import.sql files uses a sequence that is not created by by a PanacheEntity, see "hibernate_sequence", this sequence is not created when using the record pattern
I tried to make it work by using custom annotations for the Fruit ID and extending from PanacheEntityBase instead:
@Entity
public class Fruit extends PanacheEntityBase {
@Id
@SequenceGenerator(name = "fruitSequence", sequenceName = "hibernate_sequence", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "fruitSequence")
public Long id;
....
}
But it also does not work, it throws the following error in the logs:
2025-07-17 10:21:48,195 ERROR [org.jbo.res.rea.ser.han.PublisherResponseHandler] (vert.x-eventloop-thread-1) Exception in SSE server handling, impossible to send it to client: java.lang.IllegalStateException: This method is normally automatically overridden in subclasses: did you forget to annotate your entity with @Entity?
at io.quarkus.hibernate.reactive.panache.common.runtime.AbstractJpaOperations.implementationInjectionMissing(AbstractJpaOperations.java:318)
at io.quarkus.hibernate.reactive.panache.PanacheEntityBase.listAll(PanacheEntityBase.java:390)
at io.quarkus.hibernate.reactive.panache.Panache.lambda$withSession$0(Panache.java:29)
at io.smallrye.context.impl.wrappers.SlowContextualFunction.apply(SlowContextualFunction.java:21)
at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.performInnerSubscription(UniOnItemTransformToUni.java:68)
at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:57)
at io.smallrye.mutiny.operators.uni.UniOnItemConsume$UniOnItemComsumeProcessor.onItem(UniOnItemConsume.java:43)
at io.smallrye.mutiny.operators.uni.UniOnItemTransform$UniOnItemTransformProcessor.onItem(UniOnItemTransform.java:43)
at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:60)
at io.smallrye.mutiny.operators.uni.UniOperatorProcessor.onItem(UniOperatorProcessor.java:47)
at io.smallrye.mutiny.operators.uni.UniOnCancellationCall$UniOnCancellationCallProcessor.onItem(UniOnCancellationCall.java:52)
at io.smallrye.mutiny.operators.uni.builders.UniCreateFromItemSupplier.subscribe(UniCreateFromItemSupplier.java:29)
at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:35)
at io.smallrye.mutiny.operators.uni.UniOnCancellationCall.subscribe(UniOnCancellationCall.java:27)
at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:35)
at io.smallrye.mutiny.operators.uni.UniOnFailureFlatMap.subscribe(UniOnFailureFlatMap.java:31)
at io.smallrye.mutiny.operators.AbstractUni.subscribe(AbstractUni.java:35)
at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.performInnerSubscription(UniOnItemTransformToUni.java:81)
at io.smallrye.mutiny.operators.uni.UniOnItemTransformToUni$UniOnItemTransformToUniProcessor.onItem(UniOnItemTransformToUni.java:57)
at io.smallrye.mutiny.operators.uni.UniOperatorProcessor.onItem(UniOperatorProcessor.java:47)
at io.smallrye.mutiny.operators.uni.builders.UniCreateFromCompletionStage$CompletionStageUniSubscription.forwardResult(UniCreateFromCompletionStage.java:63)
at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863)
at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841)
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
at org.hibernate.reactive.pool.impl.SqlClientPool.lambda$completionStage$2(SqlClientPool.java:173)
at io.vertx.core.impl.future.FutureImpl$4.onSuccess(FutureImpl.java:176)
at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:66)
at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:259)
at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:40)
at io.vertx.core.impl.future.FutureBase.emitSuccess(FutureBase.java:66)
at io.vertx.core.impl.future.FutureImpl.tryComplete(FutureImpl.java:259)
at io.vertx.core.impl.future.Mapping.onSuccess(Mapping.java:40)
at io.vertx.core.impl.future.FutureBase.lambda$emitSuccess$0(FutureBase.java:60)
at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)
at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)
at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:840)
I only made it work by switching from record pattern to repository pattern