mn create-app --jdk=17 \
example.micronaut.micronautguide \
--features=data-jdbc,postgres,liquibase \
--build=maven \
--lang=java \
Table of Contents
- 1. Getting Started
- 2. What you will need
- 3. Solution
- 3.1. Enable annotation Processing
- 3.2. Immutable Configuration with Java Records
- 3.3. Database Migration with Liquibase
- 3.4. Mapped Entities with Java Records
- 3.5. Projections with Java Records
- 3.6. JSON serialization with Java Records
- 3.7. Testing with PostgreSQL via TestContainers
- 3.8. Create a Test
- 3.9. Running the application
- 4. Generate a Micronaut Application Native Image with GraalVM
- 5. Next steps
- 6. Help with the Micronaut Framework
Micronaut Data and Java Records
Learn how to a leverage Java records for immutable configuration, Micronaut Data Mapped Entities and Projection DTOs
Authors: Sergio del Amo
Micronaut Version: 3.5.2
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
You are going to use Record Classes in a Micronaut application.
Record classes, which are a special kind of class, help to model plain data aggregates with less ceremony than normal classes.
A record declaration specifies in a header a description of its contents; the appropriate accessors, constructor, equals, hashCode, and toString methods are created automatically. A record’s fields are final because the class is intended to serve as a simple "data carrier."
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 17 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
To use Java records, specify JDK 17 when you create the application. |
If you don’t specify the --build argument, Gradle is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
If you use Micronaut Launch, select "Micronaut Application" as application type and add postgres , data-jdbc , and liquibase as features.
|
3.1. Enable annotation Processing
If you use Java or Kotlin and IntelliJ IDEA, make sure to enable annotation processing.

3.2. Immutable Configuration with Java Records
package example.micronaut;
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.core.annotation.NonNull;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@ConfigurationProperties("vat") (1)
public record ValueAddedTaxConfiguration(
@NonNull @NotNull BigDecimal percentage) {
}
1 | The @ConfigurationProperties annotation takes the configuration prefix. |
Write a test:
package example.micronaut;
import io.micronaut.context.BeanContext;
import io.micronaut.context.annotation.Property;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Property(name = "vat.percentage", value = "21.0") (1)
@MicronautTest(startApplication = false) (2)
class ValueAddedTaxConfigurationTest {
@Inject
BeanContext beanContext;
@Test
void immutableConfigurationViaJavaRecords() {
assertTrue(beanContext.containsBean(ValueAddedTaxConfiguration.class));
assertEquals(new BigDecimal("21.0"),
beanContext.getBean(ValueAddedTaxConfiguration.class).percentage());
}
}
1 | Annotate the class with @Property to supply configuration to the test. |
2 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context. This test does not need the embedded server. Set startApplication to false to avoid starting it. |
3.3. Database Migration with Liquibase
We need a way to create the database schema. For that, we use Micronaut integration with Liquibase.
Add the following snippet to include the necessary dependencies:
<dependency>
<groupId>io.micronaut.liquibase</groupId>
<artifactId>micronaut-liquibase</artifactId>
<scope>compile</scope>
</dependency>
Configure the database migrations directory for Liquibase in application.yml
.
liquibase:
enabled: true
datasources:
default:
change-log: 'classpath:db/liquibase-changelog.xml'
Create the following files with the database schema creation and a book:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="changelog/01-create-books-schema.xml" relativeToChangelogFile="true"/>
<include file="changelog/02-insert-book.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="01" author="sdelamo">
<createTable tableName="book" remarks="A table to contain all books">
<column name="isbn" type="varchar(255)">
<constraints nullable="false" unique="true" primaryKey="true"/>
</column>
<column name="title" type="varchar(255)">
<constraints nullable="false"/>
</column>
<column name="price" type="NUMERIC">
<constraints nullable="false"/>
</column>
<column name="about" type="LONGVARCHAR">
<constraints nullable="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet id="02" author="sdelamo">
<insert tableName="book">
<column name="isbn">0321601912</column>
<column name="title">Continuous Delivery</column>
<column name="price">39.99</column>
<column name="about">Winner of the 2011 Jolt Excellence Award! Getting software released to users is often a painful, risky, and time-consuming process. This groundbreaking new book sets out the principles and technical practices that enable rapid, incremental delivery of high quality, valuable new functionality to users.</column>
</insert>
</changeSet>
</databaseChangeLog>
During application startup, Liquibase executes the SQL file, creates the schema needed for the application and inserts one book.
3.4. Mapped Entities with Java Records
Create a Micronaut Data Mapped Entity
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
@MappedEntity (1)
public record Book(@Id @NonNull @NotBlank @Size(max = 255) String isbn, (2)
@NonNull @NotBlank @Size(max = 255) String title, (3)
@NonNull @NotNull BigDecimal price, (3)
@Nullable String about) { (4)
}
1 | Annotate the class with @MappedEntity to map the class to the table defined in the schema. |
2 | The primary key is annotated with @Id . It is an assigned primary key. |
3 | Use javax.validation.constraints Constraints to ensure the data matches your expectations. |
4 | Annotate about with @Nullable , since it is optional. |
3.5. Projections with Java Records
Create a record to project some data from the book
table. For example, exclude the about
field.
package example.micronaut;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.math.BigDecimal;
@Introspected (1)
public record BookCard(@NonNull @NotBlank @Size(max = 255) String isbn, (2)
@NonNull @NotBlank @Size(max = 255) String title, (2)
@NonNull @NotNull BigDecimal price) {
}
1 | Annotate the class with @Introspected to generate BeanIntrospection metadata at compilation time. This information can be used, for example, to the render the POJO as JSON using Jackson without using reflection. |
2 | Use javax.validation.constraints Constraints to ensure the data matches your expectations. |
Create a Repository, which uses the previous Java record as a DTO projection.
package example.micronaut;
import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;
import java.util.List;
@JdbcRepository(dialect = Dialect.POSTGRES) (1)
public interface BookRepository extends CrudRepository<Book, String> { (2)
List<BookCard> find(); (3)
}
1 | @JdbcRepository with a specific dialect. |
2 | By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations. |
3 | Micronaut Data supports reflection-free Data Transfer Object (DTO) projections if the return type is annotated with @Introspected . |
3.6. JSON serialization with Java Records
Create a Java record to represent a JSON response:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.ReflectiveAccess;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
@Introspected (1)
@ReflectiveAccess (2)
public record BookForSale(@NonNull @NotBlank String isbn, (3)
@NonNull @NotBlank String title,
@NonNull @NotNull BigDecimal price) { }
1 | Annotate the class with @Introspected to generate BeanIntrospection metadata at compilation time. This information can be used, for example, to the render the POJO as JSON using Jackson without using reflection. |
2 | Jackson requires reflection to use Records. With @ReflectiveAccess the Micronaut framework will add the class to the GraalVM reflect-config.json file. |
3 | Use javax.validation.constraints Constraints to ensure the data matches your expectations. |
Create a Controller that uses the previous record:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
@Controller("/books") (1)
class BookController {
private final BookRepository bookRepository;
private final ValueAddedTaxConfiguration valueAddedTaxConfiguration;
BookController(BookRepository bookRepository, (2)
ValueAddedTaxConfiguration valueAddedTaxConfiguration) {
this.bookRepository = bookRepository;
this.valueAddedTaxConfiguration = valueAddedTaxConfiguration;
}
@ExecuteOn(TaskExecutors.IO) (3)
@Get (4)
List<BookForSale> index() { (5)
return bookRepository.find()
.stream()
.map(bookCard -> new BookForSale(bookCard.isbn(),
bookCard.title(),
salePrice(bookCard)))
.collect(Collectors.toList());
}
@NonNull
private BigDecimal salePrice(@NonNull BookCard bookCard) {
return bookCard.price()
.add(bookCard.price()
.multiply(valueAddedTaxConfiguration.percentage()
.divide(new BigDecimal("100.0"), 2, RoundingMode.HALF_DOWN)))
.setScale(2, RoundingMode.HALF_DOWN);
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books . |
2 | Use constructor injection to inject a bean of type BookRepository . |
3 | It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the Event loop. |
4 | The @Get annotation maps the index method to an HTTP GET request on '/' . |
5 | Respond a Java Record and the application responds JSON representation of the object |
3.7. Testing with PostgreSQL via TestContainers
We use Test Containers to test against a PostgreSQL database.
Add the following snippet to include the necessary test container dependencies:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
Configure the default datasource to use the PostgreSQL database provided by TestContainers for the test environment:
datasources:
default:
url: jdbc:tc:postgresql:12:///postgres
driverClassName: org.testcontainers.jdbc.ContainerDatabaseDriver (1)
1 | By using a specially modified JDBC URL, Testcontainers provides a disposable stand-in database that can be used without requiring modification to your application code. See Test Containers JDBC URL. |
3.8. Create a Test
Create a test:
package example.micronaut;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Property(name = "vat.percentage", value = "21.0") (1)
@MicronautTest(transactional = false) (2)
class BookControllerTest {
int booksInsertedInDbByMigration = 1;
@Inject
@Client("/")
HttpClient httpClient; (3)
@Inject
BookRepository bookRepository;
@Test
void recordsUsedForJsonSerialization() {
String title = "Building Microservices";
String isbn = "1491950358";
(4)
String about = """
Distributed systems have become more fine-grained in the past 10 years, shifting from code-heavy monolithic applications to smaller, self-contained microservices. But developing these systems brings its own set of headaches. With lots of examples and practical advice, this book takes a holistic view of the topics that system architects and administrators must consider when building, managing, and evolving microservice architectures.
Microservice technologies are moving quickly. Author Sam Newman provides you with a firm grounding in the concepts while diving into current solutions for modeling, integrating, testing, deploying, and monitoring your own autonomous services. You’ll follow a fictional company throughout the book to learn how building a microservice architecture affects a single domain.
Discover how microservices allow you to align your system design with your organization’s goals
Learn options for integrating a service with the rest of your system
Take an incremental approach when splitting monolithic codebases
Deploy individual microservices through continuous integration
Examine the complexities of testing and monitoring distributed services
Manage security with user-to-service and service-to-service models
Understand the challenges of scaling microservice architectures
""";
Book b = new Book(isbn,
title,
new BigDecimal("38.15"),
about);
Book book = bookRepository.save(b);
assertEquals(booksInsertedInDbByMigration + 1, bookRepository.count());
BlockingHttpClient client = httpClient.toBlocking();
List<BookForSale> books = client.retrieve(HttpRequest.GET("/books"),
Argument.listOf(BookForSale.class)); (5)
assertNotNull(books);
assertEquals(booksInsertedInDbByMigration + 1, books.size());
assertEquals("Building Microservices", books.get(1).title());
assertEquals("1491950358", books.get(1).isbn());
assertEquals(new BigDecimal("46.16"), books.get(1).price());
bookRepository.delete(book);
}
}
1 | Annotate the class with @Property to supply configuration to the test. |
2 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. By default, each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour is is changed by setting transaction to false . |
3 | Inject the HttpClient bean and point it to the embedded server. |
4 | Multiline string |
5 | Micronaut HTTP Client simplifies binding a JSON array to a list of POJOs by using Argument::listOf . |
3.9. Running the application
After installing Docker, execute the following command to run a PostgreSQL container:
docker run -it --rm \
-p 5432:5432 \
-e POSTGRES_USER=dbuser \
-e POSTGRES_PASSWORD=theSecretPassword \
-e POSTGRES_DB=postgres \
postgres:11.5-alpine
Set up the following environment variables to connect to the PostgreSQL database you started with Docker.
datasources:
default:
url: jdbc:postgresql://localhost:5432/postgres (1)
driverClassName: org.postgresql.Driver (2)
dialect: POSTGRES (3)
schema-generate: NONE (4)
1 | The JDBC URL matches the database name you used in the previous command. |
2 | Use PostgreSQL driver. |
3 | Configure the PostgreSQL dialect. |
4 | You handle database migrations via Liquibase |
export DATASOURCES_DEFAULT_USERNAME=dbuser
export DATASOURCES_DEFAULT_PASSWORD=theSecretPassword
export VAT_PERCENTAGE=20
Configure your default datasource to use the PostgreSQL database you started with Docker:
To run the application, use the ./mvnw mn:run
command, which starts the application on port 8080.
You can run a cURL command to test the application:
curl http://localhost:8080/books
[{"isbn":"0321601912","title":"Continuous Delivery","price":47.99}]
4. Generate a Micronaut Application Native Image with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native image of our Micronaut application.
Compiling native images ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
4.1. Native image generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.1.0.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
To generate a native image using Maven, run:
./mvnw package -Dpackaging=native-image
The native image is created in the target
directory and can be run with target/micronautguide
.
Due to a bug with GraalVM and Java Records it is necessary to include the flag --report-unsupported-elements-at-runtime when building the native image. Create the file native-image.properties :
|
Args = --report-unsupported-elements-at-runtime
You can run a cURL command to test the application:
curl http://localhost:8080/books
[{"isbn":"0321601912","title":"Continuous Delivery","price":47.99}]
You receive an empty array because there are no books in the database. You can create a Liquibase changelog to add seed data.
5. Next steps
Explore more features with Micronaut Guides.
6. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.