Fixing Panache: How Quarkus Unifies Blocking and Reactive Hibernate
Build a bookstore API while exploring stateless sessions, reactive repositories, and build-time validated queries.
If you have been writing Quarkus applications for a while, you know that Hibernate ORM with Panache has been one of its best-loved features. It removed mountains of boilerplate, gave you the active record pattern, and let you write readable, concise persistence code. But it accumulated some real design debts over time. A split between blocking and reactive flavours, no type-safe query support, and awkward static methods that broke method references.
In December 2025, Stéphane Épardaud (@FroMage) merged a new experimental extension into the Quarkus main branch: Hibernate with Panache Next. It unifies both the blocking and reactive worlds, adds first-class support for Jakarta Data 1.0 (the new Jakarta EE 11 standard for repository-style persistence), and introduces type-safe query annotations backed by build-time validation.
This tutorial will walk you through:
Why the old API had limits and what Panache Next solves
How Jakarta Data differs philosophically from classic Panache
A hands-on bookstore REST API built step-by-step with Panache Next
Mixing blocking and reactive sessions in the same application
Type-safe queries with
@Findand@HQLTesting strategies
Experimental: Panache Next is experimental. Package names, class names, and APIs will likely change before GA. Use it to explore the direction, and file feedback on GitHub or Quarkus Zulip.
Why a New API? The Limits of Classic Panache
The Two-Headed Dragon: ORM vs Reactive Panache
Until Panache Next, Quarkus shipped two fully separate Panache modules that were incompatible by design:
io.quarkus:quarkus-hibernate-orm-panache— extendsPanacheEntity(blocking JDBC)io.quarkus:quarkus-hibernate-reactive-panache— extendsReactivePanacheEntity(Mutiny/Vert.x)
Once Quarkus started supporting mixed blocking and reactive usage in the same application, this split became painful. If your domain model was declared with PanacheEntity, every repository that touched it had to be blocking, even when you wanted one fast reactive read endpoint.
Managed Sessions Only
Classic Panache relies exclusively on Hibernate’s managed session (the EntityManager), which tracks all loaded entities for dirty checking and lazy-load resolution. This is convenient, but it carries overhead. Stateless sessions (StatelessSession in Hibernate, which underlies Jakarta Data) skip dirty checking, give you explicit insert/update/delete, and can be much faster for bulk read paths. There was simply no way to opt into a stateless session through the old Panache API.
No Type-Safe Queries
All Panache queries, whether on active-record entities or repositories, were JPQL or HQL strings embedded in Java source code as plain string literals. There was zero compile-time checking. A typo like "where naem = ?1" would only surface at runtime. Jakarta Data’s @Find and @HQL annotations, by contrast, are validated by Hibernate’s annotation processor at build time. Panache Next brings exactly this to Quarkus.
The Active Record / Repository Split
Old Panache offered two patterns: the Active Record pattern (entity extends PanacheEntity, queries as static methods) and the Repository pattern (separate class implements PanacheRepository). They were functionally equivalent but lived in different mental models, and switching between them was confusing for teams. Static methods also had a generic type hole: you could not write var list = Person.listAll(); you had to write List<Person> list = Person.listAll(). Method references on static methods did not work at all.
Static Method Limitations
Because active-record static methods are generated by bytecode manipulation at build time, Java’s reflection and method-reference machinery could not see them as normal methods. This caused real friction when trying to pass Person::findByEmail as a function reference, for example in stream pipelines or reactive chains.
Jakarta Data vs Traditional Panache
Before writing any code, it is worth understanding the philosophical difference between how Panache has historically worked and what Jakarta Data 1.0 brings. Panache Next lets you choose session type (managed vs stateless) and I/O (blocking vs reactive) independently.
Traditional Panache: Managed Session, Implicit State
The entity lifecycle in classic Panache is stateful:
Load an entity → it becomes managed
Change a field → Hibernate detects the change via dirty checking
End of
@Transactionalmethod → flush automatically persists the change
This is convenient and you do not need to explicitly call update() or merge(). But it implies a persistent context living in memory, tracking every loaded entity. For high-read endpoints loading thousands of records, this overhead matters.
// Classic Panache: change detected automatically
@Transactional
public Book updateTitle(Long id, String newTitle) {
Book book = Book.findById(id); // managed — Hibernate watches it
book.title = newTitle; // dirty check will flush this
return book; // no explicit persist/merge needed
}Jakarta Data: Stateless Session, Explicit Operations
Jakarta Data 1.0 is built on Hibernate’s StatelessSession. Entities loaded through a Jakarta Data repository are detached from the moment they are returned. There is no dirty checking, no lazy-load tracking, and no implicit flush.
You must explicitly call
insert(),update(), ordelete()No implicit lazy loading — relations must be fetched eagerly or via
JOIN FETCHMuch lower per-request memory overhead for read-heavy workloads
Queries are type-safe at build time via annotation processing
// Jakarta Data / Panache Next stateless style (blocking)
public Book updateTitle(Long id, String newTitle) {
return SessionOperations.withStatelessTransaction(() -> {
Book book = repo.findById(id);
if (book == null) throw new NotFoundException();
book.title = newTitle;
repo.update(book);
return book;
});
}Which Should You Use?
Panache Next gives you both worlds inside a single API:
Managed blocking: Best for write-heavy transactional flows where dirty-check convenience outweighs overhead.
Managed reactive: When you need non-blocking I/O but still want implicit dirty checking.
Stateless blocking: Bulk inserts/deletes, ETL jobs, or batch processing where you want minimal overhead.
Stateless reactive: High-throughput read APIs, streaming endpoints, or CQRS read models.
Rule of thumb: Start with managed blocking (the default). Switch to stateless when profiling shows the persistence context is a bottleneck, or when designing dedicated read-model repositories.
Two Axes: Session Type and I/O
Panache Next mixes two independent choices. Keeping them explicit helps when reading and naming code:
Session type
Managed: Hibernate tracks entities; dirty checking and lazy loading apply. Use
@Transactional(blocking) or@WithTransaction(reactive).Stateless: No persistence context; entities are detached once loaded. Explicit
insert()/update()/delete(). No lazy loading—use EAGER orJOIN FETCH.
I/O
Blocking: Synchronous JDBC; methods return entities or collections directly.
Reactive: Non-blocking; methods return
Uni<T>(or similar). Runs on Vert.x event loop.
So you get four modes: managed blocking, managed reactive, stateless blocking, stateless reactive. The tutorial uses managed blocking for Author (writes and simple reads) and stateless reactive for Book (high-throughput catalogue reads).
Two Entity Base Styles
How you declare the entity matches how you intend to use it:
Managed-style entity: Extends
PanacheEntity(fromio.quarkus.hibernate.panache). Has instance methods such aspersist(),statelessBlocking(),managedReactive(), etc. Use withPanacheRepository<Entity>(blocking managed) or other managed/reactive repository types. Best when the same entity is used in transactional, write-oriented code where dirty checking is useful.Stateless-style entity: Extends only
WithId.AutoLong(or anotherWithIdvariant) and does not implement a Panache entity interface. No instance persistence methods; all access goes through a repository. Use withPanacheRepository.Reactive.Stateless<Entity, Id>(or blocking stateless) when the entity is used only for read-heavy or explicit-write flows and you want to avoid the overhead of a managed context.
You can mix both styles in one application: e.g. Author as managed-style (blocking managed Repo + optional reactive stateless ReadRepo) and Book as stateless-style with a single reactive stateless Repo.
Naming Repository Interfaces
Name repository interfaces so that session type and I/O are obvious at the call site:
Blocking managed: A single interface for all blocking, transactional access is often enough: e.g.
Repo, orAuthorRepo. Use it with@Transactionaland standard blocking return types.Reactive stateless: When you add a second interface for non-blocking, stateless access, give it a name that signals both “reactive” and “read/stateless”: e.g.
ReadRepo,CatalogRepo, orQueryRepo. That way, injectingAuthor.ReadRepomakes it clear you’re in reactive, stateless territory without reading theextendsclause.
The bookstore uses Author.Repo (blocking managed) and Author.ReadRepo (reactive stateless); Book has only one repository, so we keep Book.Repo even though it is reactive stateless.
Hands-On Tutorial: A Bookstore REST API
We will build a small bookstore application exposing a REST API to manage Authors and Books. Along the way we will demonstrate every mode Panache Next supports.
Prerequisites
Java 21+
Maven 3.9+
Podman or Docker (for a local PostgreSQL via Testcontainers Dev Services)
Quarkus CLI (optional but handy):
brew install quarkusio/tap/quarkus
Bootstrap the Project
Use the Quarkus CLI or Maven to scaffold the project. You can also directly start from my Github repository. This example was built with Quarkus 3.31.3 and the following dependencies in pom.xml:
quarkus-hibernate-panache-nextquarkus-jdbc-postgresqlquarkus-reactive-pg-clientquarkus-rest-jackson,quarkus-restquarkus-hibernate-reactivequarkus-hibernate-reactive-panache-common
Note: The extension artifact ID quarkus-hibernate-panache-next may change before GA. Always check quarkus.io/extensions for the current name.
You must configure the Hibernate annotation processor for type-safe queries. If you use the project scaffolding it should already be there but that support was merged just before I wrote this tutorial, so I am calling it out explicitly here.
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPathsUseDepMgmt>true</annotationProcessorPathsUseDepMgmt>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>Configure the Datasource
Add the following to src/main/resources/application.properties:
# Quarkus Dev Services spins up a Postgres container automatically.
# No URL needed in dev mode — just declare the db-kind.
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.schema-management.strategy=drop-and-create
quarkus.hibernate-orm.log.sql=trueDefine the Author Entity
We use a managed-style entity and two repository interfaces: one blocking managed (the default for writes and simple reads) and one reactive stateless (for read-heavy endpoints). Naming them Repo and ReadRepo makes session type and I/O clear at the call site (see Two Axes and Naming).
Create src/main/java/com/example/Author.java:
package com.example;
import java.util.List;
import java.util.Optional;
import org.hibernate.annotations.processing.Find;
import org.hibernate.annotations.processing.HQL;
import io.quarkus.hibernate.panache.PanacheEntity;
import io.quarkus.hibernate.panache.PanacheRepository;
import io.smallrye.mutiny.Uni;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "authors")
public class Author extends PanacheEntity {
@Column(nullable = false)
public String name;
public String country;
// Blocking managed — one repo for all blocking operations
public interface Repo extends PanacheRepository<Author> {
@Find
Optional<Author> findByName(String name);
@HQL("where country = :country order by name")
List<Author> findByCountry(String country);
@HQL("delete from Author where country = :country")
long deleteByCountry(String country);
}
// Reactive stateless — for read-heavy endpoints (returns Uni, hence the import above)
public interface ReadRepo
extends PanacheRepository.Reactive.Stateless<Author, Long> {
@HQL("where country = :c order by name")
Uni<List<Author>> catalog(String c);
}
}Key observations:
Entity: Extends
PanacheEntity(managed-style)—same type name as classic Panache, but a new package: classic isio.quarkus.hibernate.orm.panache; Panache Next isio.quarkus.hibernate.panache(one level up fromorm). The “Next” is the extension name.Repo: Blocking managed repository; use with
@Transactional, returns plain types. This is the primary repo for Author.ReadRepo: Reactive stateless; extends
PanacheRepository.Reactive.Stateless<Author, Long>, returnsUni<…>. The name signals “reactive + read/stateless” so call sites stay clear.@Findand@HQLare validated at build time by Hibernate’s annotation processor. Addimport io.smallrye.mutiny.Unifor reactive return types.
Define the Book Entity
Book is a stateless-style entity: it extends WithId.AutoLong only (no PanacheEntity), so it has no instance persistence methods and is used only via the repository. We expose a single reactive stateless repository, ideal for a high-traffic catalogue. We name it Repo because there is only one repository for Book; if we added a blocking managed repo later, we’d keep Repo for the primary one and use something like ReadRepo for reactive stateless (same naming idea as Author).
Create src/main/java/com/example/Book.java:
package com.example;
import java.util.List;
import org.hibernate.annotations.processing.Find;
import org.hibernate.annotations.processing.HQL;
import io.quarkus.hibernate.panache.PanacheRepository;
import io.quarkus.hibernate.panache.WithId;
import io.smallrye.mutiny.Uni;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@Table(name = "books")
public class Book extends WithId.AutoLong {
@Column(nullable = false)
public String title;
public int year;
@ManyToOne(fetch = FetchType.EAGER) // must be EAGER in stateless mode
@JoinColumn(name = "author_id")
public Author author;
public interface Repo
extends PanacheRepository.Reactive.Stateless<Book, Long> {
@Find
Uni<List<Book>> findByTitle(String title);
@HQL("where year >= :year order by year desc")
Uni<List<Book>> findPublishedSince(int year);
@HQL("delete from Book where year < :year")
Uni<Integer> deleteOlderThan(int year);
}
}Key difference: Stateless-style entity: Book extends WithId.AutoLong only, so no instance persistence methods—all access via Book.Repo. The repository is reactive stateless (PanacheRepository.Reactive.Stateless<Book, Long>); return types are Uni<>. Associations must be FetchType.EAGER (or JOIN FETCH in HQL) because stateless sessions do not support lazy loading.
Build the REST Resources
Author resource (blocking managed): Create src/main/java/com/example/AuthorResource.java:
package com.example;
import java.util.List;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/authors")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthorResource {
@Inject
Author.Repo repo;
@GET
public List<Author> list() {
return repo.findAll().list();
}
@GET
@Path("/country/{country}")
public List<Author> byCountry(@PathParam("country") String country) {
return repo.findByCountry(country);
}
@POST
@Transactional
public Response create(Author author) {
repo.persist(author);
return Response.status(201).entity(author).build();
}
@PUT
@Path("/{id}")
@Transactional
public Author update(@PathParam("id") Long id, Author patch) {
Author existing = repo.findById(id);
if (existing == null)
throw new NotFoundException();
existing.name = patch.name;
existing.country = patch.country;
return existing; // managed session: dirty check handles it
}
@DELETE
@Path("/{id}")
@Transactional
public void delete(@PathParam("id") Long id) {
repo.deleteById(id);
}
}Book resource (reactive stateless): For stateless reactive repositories you must run each operation inside a stateless session. Use SessionOperations.withStatelessTransaction(...). You can also use @WithTransaction(stateless = true) for stateless; note that both are expected to be superseded by @Transactional in a future release.
Create src/main/java/com/example/BookResource.java:
package com.example;
import java.util.List;
import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.Response;
@Path("/books")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookResource {
@Inject
Book.Repo repo;
@GET
public Uni<List<Book>> list() {
return SessionOperations.withStatelessTransaction(() -> repo.findAll().list());
}
@GET
@Path("/since/{year}")
public Uni<List<Book>> since(@PathParam("year") int year) {
return SessionOperations.withStatelessTransaction(() -> repo.findPublishedSince(year));
}
@POST
public Uni<Response> create(Book book) {
return SessionOperations.withStatelessTransaction(
() -> repo.insert(book).map(b -> Response.status(201).entity(b).build()));
}
@PUT
@Path("/{id}")
public Uni<Book> update(@PathParam("id") Long id, Book patch) {
return SessionOperations.withStatelessTransaction(() -> {
Book existing = repo.findById(id);
if (existing == null)
return Uni.createFrom().failure(new NotFoundException());
existing.title = patch.title;
existing.year = patch.year;
return repo.update(existing).replaceWith(existing);
});
}
@DELETE
@Path("/older-than/{year}")
public Uni<Integer> cleanup(@PathParam("year") int year) {
return SessionOperations.withStatelessTransaction(() -> repo.deleteOlderThan(year));
}
}Note: On stateless reactive repositories, findById currently returns the entity type directly (e.g. Book), not Uni<Book>. Reactive persist() returns Uni<Void>, so code often uses .replaceWith(entity) to pass the entity along; the team is considering changing persist() to return Uni<Entity>. Feedback or issues requesting this are welcome. The extension is experimental and the team welcomes input.
Using the Metamodel Accessor
Panache Next’s annotation processor generates a metamodel class (e.g. Author_) at build time. You can use Author_.repo() to access the repository without injection which is handy in domain services or static-style code.
Create src/main/java/com/example/AuthorService.java:
package com.example;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
@ApplicationScoped
public class AuthorService {
public void printAllBritish() {
Author_.repo()
.findByCountry("UK")
.forEach(a -> System.out.println(a.name));
}
@Transactional
public long promoteBritishAuthors() {
return Author_.repo()
.findByCountry("UK")
.stream()
.peek(a -> a.name = a.name + " (Featured)")
.count();
}
@Transactional
public void transferAuthors(String fromCountry, String toCountry) {
Author_.repo()
.findByCountry(fromCountry)
.forEach(a -> a.country = toCountry);
}
}Mixing Modes on the Same Entity
You can define multiple repository interfaces for the same entity and use both blocking managed and reactive stateless in one app. Following the naming convention: Repo = blocking managed, ReadRepo = reactive stateless. Example service that uses both on Author:
Create src/main/java/com/example/AuthorMixedModeService.java:
package com.example;
import java.util.List;
import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
@ApplicationScoped
public class AuthorMixedModeService {
@Inject
Author.Repo repo;
@Inject
Author.ReadRepo readRepo;
// Blocking managed — dirty check, no explicit update()
@Transactional
public Author createAuthor(String name, String country) {
Author a = new Author();
a.name = name;
a.country = country;
repo.persist(a);
return a;
}
@Transactional
public void renameAuthor(Long id, String newName) {
Author a = repo.findById(id);
if (a == null)
throw new NotFoundException();
a.name = newName;
}
// Blocking stateless — explicit insert/update; each stateless operation runs in its own transaction
public void bulkImport(List<Author> authors) {
authors.forEach(a -> a.statelessBlocking().insert());
}
public void bulkUpdateCountry(List<Author> authors, String country) {
authors.forEach(a -> {
a.country = country;
a.statelessBlocking().update();
});
}
// Reactive managed — non-blocking, dirty check
@WithTransaction
public Uni<Author> createAuthorReactive(String name, String country) {
Author a = new Author();
a.name = name;
a.country = country;
return a.managedReactive().persist().replaceWith(a);
}
@WithTransaction
public Uni<Void> renameAuthorReactive(Long id, String newName) {
Author a = repo.findById(id);
if (a == null)
return Uni.createFrom().failure(new NotFoundException());
a.name = newName;
return Uni.createFrom().voidItem();
}
// Reactive stateless — use SessionOperations, not @WithTransaction
public Uni<List<Author>> getCatalogByCountry(String country) {
return SessionOperations.withStatelessTransaction(() -> readRepo.catalog(country));
}
public Uni<Void> bulkImportReactive(List<Author> authors) {
return SessionOperations.withStatelessTransaction(() ->
Uni.join()
.all(authors.stream()
.map(a -> a.statelessReactive().insert())
.toList())
.andFailFast()
.replaceWithVoid());
}
}Findings from this service:
Managed sessions (blocking or reactive): Use
@Transactional(blocking) or@WithTransaction(reactive) so that persistence runs inside a transaction.Stateless blocking (
statelessBlocking().insert()/update()): Each call uses its own stateless session and transaction; do not wrap the calling method in@Transactional.Reactive stateless (
readRepo.catalog,statelessReactive().insert()): Wrap inSessionOperations.withStatelessTransaction(...)(or@WithTransaction(stateless = true)).Reactive managed (
@WithTransaction,managedReactive().persist()): Use@WithTransactionas usual.
Running and Testing
Start the application in dev mode:
quarkus devQuarkus Dev Services will start a PostgreSQL container. Use the Dev UI at http://localhost:8080/q/dev to explore entities and run queries.
Author tests:
# Create an author
curl -s -X POST http://localhost:8080/authors \
-H 'Content-Type: application/json' \
-d '{"name":"Ursula K. Le Guin","country":"USA"}' | jq
# List all authors
curl -s http://localhost:8080/authors | jqBook tests:
# Create a book (author must exist, e.g. id 1)
curl -s -X POST http://localhost:8080/books \
-H 'Content-Type: application/json' \
-d '{"title":"The Left Hand of Darkness","year":1969,"author":{"id":1}}' | jq
# Find books published since 1970
curl -s http://localhost:8080/books/since/1970 | jq
# Clean up old books
curl -s -X DELETE http://localhost:8080/books/older-than/1960Writing Tests
Blocking managed (Author): Use @TestTransaction so each test rolls back.
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import io.quarkus.test.TestTransaction;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
@QuarkusTest
class AuthorRepositoryTest {
@Inject
Author.Repo repo;
@Test
@TestTransaction
void shouldPersistAndFindByName() {
Author a = new Author();
a.name = "Octavia Butler";
a.country = "USA";
repo.persist(a);
Optional<Author> found = repo.findByName("Octavia Butler");
assertTrue(found.isPresent());
assertEquals("USA", found.get().country);
}
@Test
@TestTransaction
void shouldCountByCountry() {
for (int i = 0; i < 3; i++) {
Author a = new Author();
a.name = "Author " + i;
a.country = "UK";
repo.persist(a);
}
assertEquals(3, repo.findByCountry("UK").size());
}
}Reactive stateless (Book): Use @RunOnVertxContext and inject a UniAsserter so the test runs on the Vert.x event loop; then use asserter.assertThat(...) to chain Uni-returning operations and assert on results. No @TestTransaction—the stateless session manages the transaction.
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import org.junit.jupiter.api.Test;
import io.quarkus.hibernate.reactive.panache.common.runtime.SessionOperations;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.vertx.RunOnVertxContext;
import io.quarkus.test.vertx.UniAsserter;
import jakarta.inject.Inject;
@QuarkusTest
class BookRepositoryTest {
@Inject
Book.Repo repo;
@Test
@RunOnVertxContext
void shouldInsertAndFindBook(UniAsserter asserter) {
var book = new Book();
book.title = "Kindred";
book.year = 1979;
asserter.assertThat(
() -> SessionOperations.withStatelessTransaction(() -> repo.insert(book)),
v -> { /* insert completed */ });
asserter.assertThat(
() -> SessionOperations.withStatelessTransaction(() -> repo.findByTitle("Kindred")),
list -> {
assertFalse(list.isEmpty());
assertEquals("Kindred", list.get(0).title);
assertEquals(1979, list.get(0).year);
});
}
}Best Practices and Pitfalls
Use EAGER (or JOIN FETCH) in stateless mode
Stateless sessions do not support lazy loading. Annotate associations withFetchType.EAGERor fetch them in HQL withJOIN FETCH.Stateless reactive: use SessionOperations.withStatelessTransaction
For stateless reactive repository calls, wrap each operation inSessionOperations.withStatelessTransaction(...). Do not use@WithTransactionfor stateless; reserve@WithTransactionfor managed reactive sessions.Do not mix @Transactional and @WithTransaction on the same method
@Transactionalis for blocking;@WithTransactionis for reactive managed. Using both on the same method causes session context issues. Using both on the same bean (on different methods) is fine.Use @TestTransaction for blocking managed tests
@TestTransactionrolls back after each test method. Use it for tests that use blocking managed repositories. For reactive stateless tests, use@RunOnVertxContextwith aUniAsserterparameter andasserter.assertThat(...)so the test runs on the Vert.x context and assertions run after eachUnicompletes.Validate names against the current release
Panache Next is experimental. Check Quarkus documentation for the current extension coordinates and package names.Share feedback
The Quarkus team welcomes input before the API is finalised. Use the Quarkus Panache Next project board on GitHub or Quarkus Zulip.
Summary
Classic Panache had real limitations: incompatible blocking/reactive APIs, no type-safe queries, and a confusing active-record/repository split.
Jakarta Data 1.0 introduces stateless-session-backed repositories with build-time-validated queries—a different and often more performant model for reads.
Panache Next unifies blocking/reactive and managed/stateless under one API, with nested repositories and a generated metamodel (
Author_, etc.).This bookstore example showed entities, repositories, mixed modes, REST endpoints, and tests aligned with the current behaviour.
Managed sessions use
@Transactional(blocking) or@WithTransaction(reactive). Stateless reactive usesSessionOperations.withStatelessTransaction(or@WithTransaction(stateless = true)); stateless blocking does not use@Transactionalon the calling method.
Next steps: try the extension with a recent Quarkus BOM (this example used 3.31.3), convert an entity to a nested Repo interface with @Find/@HQL, and report what you find. Your feedback will help shape the API.


