Hybrid Persistence in Quarkus: Blocking, Reactive, and Panache Next in One Codebase

This series is based on hands-on migration work, not on a clean-room framework comparison.

That matters, because the most useful things I learned did not come from reading feature lists. They came from trying to move a real Quarkus application forward without breaking everything around it.

The main lesson was simple:

I got much better results once I stopped asking, "Should this app be blocking or reactive?" and started asking, "Which parts of this app actually need to change first?"

That is the frame for this series and for the example code that supports it.

Repo branch for this article: article-1-baseline

Initial starting point: base

Hybrid persistence overview

The Mistake in the Usual Discussion

A lot of discussions around Quarkus persistence get trapped in the wrong shape.

They tend to sound like this:

In practice, none of those choices is precise enough to help much.

What helped me much more was looking at the system by workload:

Once I started looking at it that way, hybrid persistence stopped feeling like a compromise and started feeling like the only realistic migration plan.

Why I Chose a Crypto-Exchange Example

The public demo repo for this series uses a compact crypto-exchange style domain:

I did not choose that domain because I care about exchange demos in general. I chose it because it naturally contains different kinds of persistence pressure in one app.

It gives me:

That is exactly the kind of mix where one execution style across the whole codebase starts to feel more ideological than practical.

What I Would Not Change First

One of the easiest things to overestimate in migration work is low-frequency CRUD.

If an endpoint is:

then I usually do not want to spend early migration energy there.

In the example repo, the admin user side is deliberately still blocking:

That is not because blocking is somehow better. It is because I do not need reactive machinery there first.

The code is straightforward and honest about what it is. The baseline example is already Panache Next, just still blocking:

@Transactional
public UserAccountView createUser(CreateUserRequest request) {
    UserAccount entity = new UserAccount();
    entity.email = request.email();
    entity.displayName = request.displayName();
    entity.countryCode = request.countryCode();
    entity.kycStatus = "PENDING";
    entity.createdAt = Instant.now();
    entity.persist();
    return mapper.toView(entity);
}

There is nothing wrong with leaving that alone while more valuable work moves first.

Where Reactive Started Making Sense

The first places where reactive work paid off for me were never the easy CRUD endpoints.

They were the paths that were:

That is why the example repo puts reactive behavior around the market side first:

Those endpoints are intentionally the first candidates to become Uni-based in the next branch:

public Uni<List<MarketView>> search(@QueryParam("q") String query) {
    return marketQueryService.searchMarkets(query);
}

and:

public Uni<List<MarketView>> searchMarkets(String query) {
    String normalizedQuery = query == null ? "" : query.trim();
    return Market_.managedReactive()
        .find("lower(symbol) like ?1 order by symbol", normalizedQuery.toLowerCase() + "%")
        .list()
        .map(mapper::toViews);
}

This is the kind of split I wish more migration discussions started with:

What Changed Once I Stopped Thinking in One Style

The biggest practical shift was not technical. It was architectural honesty.

As soon as I allowed blocking and reactive paths to coexist intentionally, the system became easier to reason about:

That last point matters a lot.

Panache Next Is the Stable Layer Here

This is the migration shape I wish I had chosen earlier:

going reactive and adopting Panache Next do not have to be two separate modeling migrations.

Panache Next is the stable modeling layer from article 1 onward.

Reactive migration then changes how selected paths execute inside that same model.

That makes the example progression much cleaner:

Why I Chose Panache Next From the Start

Quarkus now has an explicit Panache Next direction:

That matters because one of the practical lessons from the migration work behind this series was that using classic ORM Panache for blocking code and reactive Panache for reactive code in parallel is possible, but it is also easy to make that mix messy.

The pain is not theoretical. In practice, you end up with:

If the migration approach is to change execution mode gradually while keeping the domain model understandable, starting with one stable model from the beginning is a better fit. Panache Next helps with that.

It also gives one thing that is very useful both in real code and in examples: generated metamodel access. That is where calls like UserAccount_.repo() for a custom blocking repository and Market_.managedReactive() for reactive access come from.

Those _ classes are not handwritten helpers. They are generated by the Hibernate processor, as described in the Panache Next guide and in the Hibernate metamodel generator documentation:

That is why the examples in this series can use a style like this on the reactive side:

return Market_.managedReactive()
    .find("lower(symbol) like ?1 order by symbol", normalizedQuery.toLowerCase() + "%")
    .list();

and a custom repository shortcut like this on the blocking admin side:

return UserAccount_.repo().listNewestFirst();

without injecting a separate top-level repository just to make one query call.

One important caveat: Panache Next is still marked experimental in the official guide at the time of writing. I think that needs to be said clearly.

So this series is not claiming “everyone should move production systems to Panache Next immediately.” It is claiming something narrower:

The Split I Actually Trust

Here is the split I trust more now than I did before doing this kind of work:

Area Style Why
Admin user management Blocking Panache Next low-frequency CRUD, low-value optimization target
Wallet maintenance Blocking Panache Next internal path
Market search Reactive Panache Next later hot read path
Recent trades Reactive Panache Next later repeated feed-style reads
Alerts and subscriptions reactive candidates inside Panache Next scheduler and candidate-lookup pressure
Entity and repo model Panache Next from the start stable baseline across the series

This is not a purity model. It is a sequencing model.

That is why I find it more useful.

Persistence decision matrix

What the Example Repo Is Trying To Show

The public repo is not trying to prove that mixed persistence is elegant in the abstract.

It is trying to show something more useful:

you can keep low-risk blocking paths in place while moving one meaningful hot path to reactive, and the application becomes easier to evolve once that split is explicit.

The example used in this series is meant to show a cleaner migration story:

That is a much better starting point for a migration story than pretending the whole app should switch styles in one move.

Closing

The hands-on lesson for me was not that reactive is always better or that blocking is somehow outdated.

The real lesson was that migration improves when you stop treating persistence as one global choice.

Different workloads deserve different treatment, and Panache Next does not have to be the thing that changes every time the execution mode changes.

That is the reason this series starts with a stable blocking baseline and then moves selected hot paths to reactive execution.

Not because it is fashionable, but because it is closer to how useful migration work actually happens.

In the next article, I will stay with that practical angle and show the first reactive step inside this Panache Next baseline: not as a rewrite, but as a controlled move on one hot path while the blocking side stays intact.

Continue Reading