Quarkus
Asynchronous Programming
Data Persistence
Java
Reactive Systems

Persist data asynchronously in Quarkus

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In Quarkus, "persist asynchronously" can mean two different things. You can either move blocking persistence work off the request thread, or you can use a fully reactive persistence stack so the database call itself is non-blocking.

Blocking ORM And Reactive Persistence Are Different

If you use classic Hibernate ORM with JPA, database access is blocking. Wrapping a blocking repository call in CompletableFuture does not make the JDBC driver non-blocking; it only moves the work to another thread.

That can still be useful for background jobs, but it is not the same as reactive persistence.

In practice, Quarkus gives you two common choices:

  • Hibernate ORM with Panache or JPA for imperative applications
  • Hibernate Reactive with Mutiny for reactive applications

Choose based on the rest of your application model. Mixing them carelessly creates transaction and thread-context problems.

Option 1: Real Reactive Persistence With Hibernate Reactive

If your application is reactive end to end, use Hibernate Reactive and return Uni from your service methods.

java
1import io.smallrye.mutiny.Uni;
2import io.quarkus.hibernate.reactive.panache.common.WithTransaction;
3import jakarta.enterprise.context.ApplicationScoped;
4
5@ApplicationScoped
6public class BookService {
7
8    @WithTransaction
9    public Uni<Book> create(String title) {
10        Book book = new Book();
11        book.title = title;
12        return book.persistAndFlush().replaceWith(book);
13    }
14}

A resource can expose that directly:

java
1import io.smallrye.mutiny.Uni;
2import jakarta.inject.Inject;
3import jakarta.ws.rs.POST;
4import jakarta.ws.rs.Path;
5
6@Path("/books")
7public class BookResource {
8
9    @Inject
10    BookService service;
11
12    @POST
13    public Uni<Book> create(BookRequest request) {
14        return service.create(request.title());
15    }
16}

This is the cleanest answer when the goal is truly asynchronous, non-blocking persistence.

Option 2: Offload Blocking Work For Background Processing

If your project uses Hibernate ORM and JDBC, treat persistence as blocking and move it into a background job only when that fits the business requirement.

For example, a REST endpoint can accept work and queue it for later processing.

java
1import jakarta.enterprise.context.ApplicationScoped;
2import java.util.concurrent.ExecutorService;
3import java.util.concurrent.Executors;
4
5@ApplicationScoped
6public class ImportDispatcher {
7    private final ExecutorService executor = Executors.newFixedThreadPool(4);
8
9    public void submit(Runnable task) {
10        executor.submit(task);
11    }
12}

Then perform the blocking persistence in a transactional service method invoked by that worker.

This pattern is appropriate for fire-and-forget imports, audit writes, or slow batch work. It is not ideal when the caller needs the created entity immediately.

Transactions Still Matter

A common source of confusion is expecting a transaction opened on one thread to remain valid after work hops to another thread. It will not. If you dispatch work asynchronously, open the transaction inside the worker-side method.

For reactive persistence, use the reactive transaction annotations and reactive session handling. For blocking persistence, keep the work inside a normal transactional service that runs on the worker thread.

The core rule is simple: the persistence model and transaction model must match the execution model.

Returning Early Versus Persisting Later

Some applications do not need to wait for the insert or update to finish before answering the user. In those cases, it can be cleaner to return 202 Accepted and process the work later.

That makes the contract explicit:

  • the request was accepted
  • the database write may happen after the response
  • the client should not assume the record already exists

This design is often better than pretending a synchronous write was asynchronous just because it ran on another executor thread.

When Messaging Is Better Than Threads

If the persistence step is part of a larger workflow, consider using a queue or broker instead of managing raw executors. Quarkus integrates well with messaging, and a message-driven consumer can perform the database write with clearer retry and failure handling.

That is usually a better architecture when work must survive application restarts or be retried safely.

Common Pitfalls

The most common mistake is assuming CompletableFuture automatically makes JPA persistence non-blocking. It does not.

Another mistake is starting a transaction on the request thread and then continuing the work on a different thread. Transaction context does not magically follow.

Developers also get into trouble by mixing reactive repositories with blocking code paths in the same service without being explicit about thread usage.

Finally, do not use in-memory executors for durable business workflows that require retries or recovery. That is what messaging infrastructure is for.

Summary

  • In Quarkus, blocking persistence and reactive persistence are different designs.
  • Use Hibernate Reactive with Mutiny for truly non-blocking database writes.
  • Use worker threads only when you intentionally offload blocking ORM work.
  • Open transactions inside the thread or reactive context that performs the write.
  • Return 202 Accepted when the write happens later.
  • Use messaging for durable asynchronous workflows, not ad hoc background threads.

Course illustration
Course illustration

All Rights Reserved.