Mastering Pagination with Quarkus Panache: A Practical Guide
Facing large result sets in Quarkus? This guide shows how to use Panache's Page API for clean pagination, highlights offset paging trade‑offs, and covers dev‑services and native‑image considerations.
16 Jan 2026, 21:37 UTC

Facing Large Result Sets in Quarkus
When an API endpoint needs to return thousands of rows, naïve “list all” queries quickly become a performance bottleneck. In a Quarkus application that uses Hibernate ORM with Panache, pagination is supported out of the box via the Page class. This article walks through a concrete example of paging, explains the trade‑offs of offset‑based pagination, and shows how to keep the code clean while staying aware of native‑image and dev‑services considerations.
1. Set Up a Minimal Project
// pom.xml (excerpt)
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache-rest</artifactId>
</dependency>
<!-- Use the driver of your choice, e.g. PostgreSQL -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
With quarkus.datasource.jdbc.url omitted, Quarkus dev services will automatically spin up a PostgreSQL container when you run ./mvnw quarkus:dev.
2. Define the Entity and Repository
Using the repository pattern keeps the data‑access layer separate from your business logic.
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.enterprise.context.ApplicationScoped;
@Entity
@Table(name = "person")
public class Person extends PanacheEntityBase {
@Id
public Long id;
public String name;
public String status; // e.g., "ACTIVE" or "INACTIVE"
}
@ApplicationScoped
public class PersonRepository implements PanacheRepository {
public Page findActive(Page page) {
return find("status", "ACTIVE").page(page).list();
}
}
3. Expose a Paginated Endpoint
Quarkus RESTEasy Reactive makes it trivial to bind query parameters to a Page instance. The Page.of(index, size) factory expects a zero‑based index, so the first page is index=0.
import io.quarkus.panache.common.Page;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/people")
public class PersonResource {
@Inject
PersonRepository repo;
@GET
@Produces(MediaType.APPLICATION_JSON)
public PageResponse findPeople(
@QueryParam("page") @DefaultValue("0") int page,
@QueryParam("size") @DefaultValue("10") int size) {
Page p = repo.findActive(Page.of(page, size));
return new PageResponse(p.list(), p.pageCount(), p.getTotalCount());
}
}
class PageResponse {
public List items;
public long pageCount;
public long totalCount;
public PageResponse(List items, long pageCount, long totalCount) {
this.items = items;
this.pageCount = pageCount;
this.totalCount = totalCount;
}
}
When a client requests /people?page=2&size=5, the server will return items 11‑15 (zero‑based indexing) and include the total number of active rows and how many pages exist.
4. Understand the Offset‑Paging Trade‑Off
Panache’s Page uses the classic OFFSET/LIMIT strategy under the hood. The advantages are:
- Simplicity – no extra state is needed on the client side.
- Direct integration with JPA/Hibernate – no custom SQL required.
- Built‑in total count support (
pageCount()andgetTotalCount()).
However, offset paging can suffer when the underlying data changes between requests. If a new row is inserted before the current offset, subsequent pages may skip or duplicate rows. In highly concurrent environments, a cursor‑based approach (e.g., using a unique key or timestamp) can provide stronger consistency at the cost of more complex client logic.
5. Dev Services & Native‑Image Considerations
Quarkus dev services simplify local development, but they also reveal a subtle issue: the Page API uses reflection to inspect entity metadata. In a GraalVM native image, this reflection is automatically registered for Panache entities, so the code above works out of the box. If you add custom JPA mappings or use vendor‑specific functions, you may need to add a reflect-config.json entry.
To verify that pagination works in native mode, build once:
./mvnw package -Pnative
./target/your-app-1.0.0-runner --dev
Then hit the endpoint and confirm the JSON structure matches the PageResponse model. If you see errors like ClassNotFoundException for entity classes, add them to reflect-config.json.
6. Takeaway & Action Items
- Use
PanacheQuery.page(Page.of(index, size))for clean, type‑safe pagination. - Expose page metadata (total count, page count) to let clients handle navigation.
- Be aware of offset‑paging drift; consider cursor‑paging for write‑heavy tables.
- Leverage Quarkus dev services to spin up a real database container for realistic testing.
- Test native builds early to surface reflection or configuration gaps.
With these practices, you can deliver responsive APIs that scale to millions of rows while keeping your codebase concise and maintainable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.