About cookies on this site Our websites require some cookies to function properly (required). In addition, other cookies may be used with your consent to analyze site usage, improve the user experience and for advertising. For more information, please review your options. By visiting our website, you agree to our processing of information as described in IBM’sprivacy statement. To provide a smooth navigation, your cookie preferences will be shared across the IBM web domains listed here.
Tutorial
Mastering data persistence with Quarkus
Discover the power of Quarkus and Dev Services for streamlined data persistence
In this tutorial, you will connect your Quarkus application to a database. You will use Hibernate ORM with Panache, which gives you a clean API for working with relational data. Quarkus will start to run PostgreSQL automatically through Dev Services so that you don’t need to install the database manually and for when you run your app in dev or test mode.
Prerequisites
Before you begin, make sure that you have installed:
- A JDK, version 17 or later. See the prerequisites in the Getting Started with Quarkus CLI tutorial.
- The Quarkus CLI. See Step 1 in the Getting Started with Quarkus CLI tutorial.
Step 1. Installing a container runtime for Dev Services
Behind the scenes, Dev Services uses Podman or Docker. These tools run lightweight containers that hold services like PostgreSQL. If you don’t already have Podman or Docker installed, Dev Services will not work, because Quarkus needs a container runtime to launch the database.
By default, the PostgreSQL image is pulled from docker.io/library/postgres, which typically does not need special access permissions. You can configure an alternative image or registry using the quarkus.datasource.devservices.image-name property. For more details, refer to the Database Dev Services configuration section.
Installing Podman
For Windows, follow the official instructions to download the Podman Windows installer.
For Linux based systems, pick your favorite packaging manager or install the binary for your platform directly from the Podman Github repository.
You can use sudo or Homebrew to install Podman:
- macOS (with Homebrew):
brew install podman - Fedora, RHEL, or CentOS:
sudo dnf install podman - Debian or Ubuntu:
sudo apt-get install podman
Then, verify your installation:
podman --version
Installing Docker
You can also use Docker. Follow the installation instructions on Docker.com.
Step 2. Add Panache and PostgreSQL extensions
You’ve already seen that Quarkus uses extensions to add capabilities. To work with relational data, you need Hibernate ORM with Panache (to simplify database access) and PostgreSQL (the database driver).
Let’s create a new project that scaffolds the with the beforementioned extensions:
quarkus create app com.ibm.developer:module-three \
--extension=quarkus-rest-jackson,quarkus-hibernate-orm-panache,quarkus-jdbc-postgresql
cd module-three
This creates a new project called “module-three” with the right extensions:
quarkus-rest-jackson- REST API with JSONquarkus-hibernate-orm-panache- JPA entities with Panachepostgresql- JDBC driver
Notice that you are not limited to PostgreSQL. Quarkus and Hibernate support various databases. Check out more details in the official Quarkus guide for Hibernate ORM.
When you now run Quarkus in dev mode, Quarkus will notice the PostgreSQL dependency and automatically start a PostgreSQL container with Dev Services.
Step 3. Create an entity class
Almost every application needs to store data in a database. In Java, this is usually done through the Java Persistence API (JPA). JPA is a standard that defines how Java objects can be mapped to database tables. On its own, JPA is just an API. It needs an implementation to do the real work.
One of the most popular implementations is Hibernate ORM. Hibernate takes care of translating between Java objects and database rows. It is powerful, but comes with powers that can make it complex.
Quarkus builds on top of Hibernate ORM and provides Panache, a simplified persistence layer. Panache reduces the amount of code that you need to write, so working with entities feels natural and lightweight.
Let’s modify and rename the scaffolded MyEntity.java to src/main/java/com/ibm/developer/Fruit.java:
package com.ibm.developer;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Fruit extends PanacheEntity {
public String name;
public String color;
}
Here’s what happens in this class:
@Entitymarks the class as a JPA entity, meaning it maps to a database table.- By extending
PanacheEntity, you automatically get an ID column and built-in methods likepersist(),findAll(), anddeleteById(). - Each field (name, color) becomes a column in the Fruit table.
This way, you can create, find, and delete objects in the database with very little code.
Step 4. Expose data through REST
Having an entity class is not enough. You need a way to access it from outside your application. REST endpoints provide this access. By exposing your entity through REST, you allow other applications or frontend code to interact with your data. You already know how to build them with Quarkus REST.
When you save, update, or delete data in a database, these actions need to happen inside a transaction. A transaction is like a safety wrapper: either all changes succeed, or none of them are applied. This ensures that your data stays consistent, even if something goes wrong.
In Quarkus, you mark methods that change data with the annotation @Transactional. This tells Quarkus and Hibernate to start and commit a transaction automatically around your method. Without it, your changes would not be written to the database.
Rename and modify the scaffolded GreetingResource to src/main/java/com/ibm/developer/FruitResource.java:
package com.ibm.developer;
import java.util.List;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/fruits")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class FruitResource {
@GET
public List<Fruit> list() {
return Fruit.listAll();
}
@POST
@Transactional
public Fruit add(Fruit fruit) {
fruit.persist();
return fruit;
}
@DELETE
@Path("/{id}")
@Transactional
public void delete(@PathParam("id") Long id) {
Fruit.deleteById(id);
}
}
Now you have three REST operations:
GET /fruitslists all fruitsPOST /fruitsadds a new fruit (inside a transaction)DELETE /fruits/{id}deletes a fruit by ID (inside a transaction)
You also notice that we access the JPA data through the Fruit class directly. This is called the “active record pattern”. If you have more complex logic or custom queries, you should look at the “repository pattern.”
Step 5. Test your persistence layer
Testing is essential to make sure that your application behaves correctly. Many Java tutorials use in-memory databases like H2 for testing, because they are easy to set up. But there is a problem: H2 is not PostgreSQL. It behaves differently, supports different SQL features, and sometimes code that works in H2 will fail in PostgreSQL.
Quarkus Dev Services solves this problem by starting a real PostgreSQL database in a container when you run your tests. This means you test against the same database you will use in production, without having to install anything manually. It gives you more realistic tests and more confidence that your code will work in real life.
Let us rename and modify the GreetingResourceTest.java to src/test/java/com/ibm/developer/FruitResourceTest.java:
package com.ibm.developer;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.hasItems;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
@QuarkusTest
public class FruitResourceTest {
@Test
public void testFruitEndpoint() {
// Insert a fruit
given()
.body("{\"name\":\"Banana\", \"color\":\"Yellow\"}")
.header("Content-Type", "application/json")
.when().post("/fruits")
.then()
.statusCode(200);
// Check that it is listed
given()
.when().get("/fruits")
.then()
.statusCode(200)
.body("name", hasItems("Banana"));
}
}
Delete the src/test/java/com/ibm/developer/GreetingResourceIT.java since we are not executing Integration Tests in this tutorial.
Make sure all the files are saved.
If your application is still running, hit “r” in the console to see the tests run in “continuous test mode”. You can also execute the tests by using Maven directly:
./mvnw test
When the test runs, Quarkus will:
- Detect that PostgreSQL is needed.
- Start a PostgreSQL Dev Service in a container using Podman or Docker.
- Run the tests against this real database.
Step 6: Test the REST operations
Start the application in dev mode
quarkus dev
Accessing the http://localhost:8080/fruits returns no fruits because none has been added yet. The fruit created while testing the persistence layer in the previous step is no longer available as by default the quarkus.hibernate-orm.schema-management.strategyconfiguration is set to drop-and-create which resets the database schema every application startup.
In a separate terminal, execute the following curl commands to add more fruits:
curl -X POST http://localhost:8080/fruits -d '{"name":"apple", "color":"red"}' -H 'Content-Type: application/json'
curl -X POST http://localhost:8080/fruits -d '{"name":"orange", "color":"orange"}' -H 'Content-Type: application/json'
curl -X POST http://localhost:8080/fruits -d '{"name":"kiwi", "color":"green"}' -H 'Content-Type: application/json'
View the newly created fruits by executing:
curl http://localhost:8080/fruits
Next, delete the fruit with id:1 by executing:
curl -X DELETE http://localhost:8080/fruits/1
Verify that only two fruits remain byexecuting:
curl http://localhost:8080/fruits
You can now stop the Quarkus dev mode pressing Ctrl+C.
A note about mvnw
So far, we used the Quarkus CLI (quarkus create app and quarkus dev) to manage and run our project. Behind the scenes, the Quarkus CLI uses Maven (or Gradle if you chose that build tool).
Every Quarkus project that is generated with the CLI includes a file called mvnw (short for Maven Wrapper). This is a small script that downloads the correct version of Maven automatically and runs it for you. You don’t need to install Maven yourself.
On Linux or macOS, you run it as ./mvnw. On Windows, you run it as mvnw.cmd. This ensures that everyone on your team uses the same Maven version, which avoids “it works on my machine” problems.
When should you use the Quarkus CLI versus ./mvnw? Use the Quarkus CLI for developer-friendly commands like create app, dev mode, or adding extensions. Use ./mvnw for standard Maven build goals like package, test, or clean.
You will often switch between the two, depending on what you want to do.
Summary
You learned how Panache simplifies database persistence by providing an active record pattern that reduces boilerplate code, and how Dev Services automatically manages database containers so you can test against production-like databases without manual setup. You also discovered how Quarkus handles transactions declaratively with @Transactional, ensuring data consistency with minimal code.