Create and Connect a Micronaut Application to an Oracle Cloud Infrastructure Autonomous Database

This guide describes how to create a Micronaut application using GCN. The application presents REST endpoints and stores data in an Oracle Cloud Infrastructure Autonomous Database using Micronaut Data.

Oracle Cloud Infrastructure Autonomous Database is a fully automated database service that makes it easy for all organizations to develop and deploy application workloads regardless of complexity, scale, or criticality.

Micronaut Data is a database access toolkit that uses ahead-of-time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer. Micronaut Data supports the following backends: JPA (Hibernate) and Hibernate Reactive; SQL (JDBC, R2DBC), and MongoDB.

Prerequisites #

Follow the steps below to create the application from scratch. However, you can also download the completed example in Java:

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project name: oci-adb-demo
    • Base Package: com.example (Default)
    • Clouds: OCI
    • Language: Java (Default)
    • Build Tool: Gradle (Groovy) or Maven
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Database
    • Features: GraalVM Native Image and Oracle Cloud Autonomous Transaction Processing (ATP)
    • Sample Code: Yes (Default)
  3. Click Generate Project. The GCN Launcher creates an application with the default package com.example in a directory named oci-adb-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.

Alternatively, use the GCN CLI as follows:

gcn create-app com.example.oci-adb-demo \
    --clouds=oci \
    --services=database \
    --features=graalvm,oracle-cloud-atp \
    --build=gradle \
gcn create-app com.example.oci-adb-demo \
    --clouds=oci \
    --services=database \
    --features=graalvm,oracle-cloud-atp \
    --build=maven \

1.1. Domain Entity #

The GCN Launcher created the sample domain entity in the lib/src/main/java/com/example/domain/ file.

package com.example.domain;

import io.micronaut.serde.annotation.Serdeable;

import jakarta.validation.constraints.NotNull;

public class Genre {

    private Long id;

    private String name;

    public Long getId() {
        return id;

    public void setId(Long id) { = id;

    public String getName() {
        return name;

    public void setName(String name) { = name;

    public String toString() {
        return "Genre{" + "id=" + id + ", name='" + name + "'}";

1.2. Repository Interface #

A repository interface defines the operations to access the database. Micronaut Data will implement the interface at compilation time. A sample repository interface was created for you in lib/src/main/java/com/example/repository/

package com.example.repository;

import com.example.domain.Genre;
import io.micronaut.core.annotation.NonNull;

import jakarta.validation.constraints.NotBlank;

import static;

@JdbcRepository(dialect = ORACLE) // <1>
public interface GenreRepository extends PageableRepository<Genre, Long> { // <2>

    Genre save(@NonNull @NotBlank String name);

    long update(@Id long id, @NonNull @NotBlank String name);

1 @JdbcRepository with a specific dialect.

2 Genre, the entity to treat as the root entity for the purposes of querying, is established either from the method signature or from the generic type parameter specified to the GenericRepository interface.

The repository extends from PageableRepository. It inherits the hierarchy PageableRepositoryCrudRepositoryGenericRepository.

Repository Description
PageableRepository A repository that supports pagination.
It provides findAll(Pageable) and findAll(Sort).
CrudRepository A repository interface for performing CRUD (Create, Read, Update, Delete).
It provides methods such as findAll(), save(Genre), deleteById(Long), and findById(Long).
GenericRepository A root interface that features no methods but defines the entity type and ID type as generic arguments.

1.3. Controller #

Hibernate Validator is a reference implementation of the Validation API. Micronaut has built-in support for validation of beans that use jakarta.validation annotations. The necessary dependencies are included by default when creating a project.

The GCN Launcher created the main controller that exposes a resource with the common CRUD operations for you in lib/src/main/java/com/example/controller/

package com.example.controller;

import com.example.domain.Genre;
import com.example.service.GenreService;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.http.annotation.Status;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import java.util.Optional;

import static io.micronaut.http.HttpHeaders.LOCATION;
import static io.micronaut.http.HttpStatus.NO_CONTENT;

@ExecuteOn(TaskExecutors.IO) // <1>
@Controller("/genres") // <2>
class GenreController {

    private final GenreService genreService;

    GenreController(GenreService genreService) { // <3>
        this.genreService = genreService;

    @Get("/{id}") // <4>
    public Optional<Genre> show(Long id) {
        return genreService.findById(id);

    @Put("/{id}/{name}") // <5>
    public HttpResponse<?> update(long id, String name) {
        genreService.update(id, name);
        return HttpResponse
                .header(LOCATION, URI.create("/genres/" + id).getPath());

    @Get("/list") // <6>
    public List<Genre> list(@Valid Pageable pageable) {
        return genreService.list(pageable);

    @Post // <7>
    public HttpResponse<Genre> save(@Body("name") @NotBlank String name) {
        Genre genre =;

        return HttpResponse
                .headers(headers -> headers.location(URI.create("/genres/" + genre.getId())));

    @Delete("/{id}") // <8>
    public void delete(Long id) {

1 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.

2 The class is defined as a controller with the @Controller annotation mapped to the path /genres.

3 Uses constructor injection to inject a bean of type GenreRepository.

4 Maps a GET request to /genres/{id}, which attempts to show a genre. This illustrates the use of a URL path variable (id).

5 Maps a PUT request to /genres/{id}/{name}, which attempts to update a genre. This illustrates the use of URL path variables (id and name).

6 Maps a GET request to /genres/list, which returns a list of genres. This mapping illustrates URL parameters being mapped to a single POJO.

7 Maps a POST request to /genres, which attempts to create a new genre.

8 Maps a DELETE request to /genres/{id}, which attempts to remove a genre. This illustrates the use of a URL path variable (id).

1.4. Service #

A service contains business logic and facilitates communication between the controller and the repository. The domain is used to communicate between the controller and service layers.

GCN created a sample service class, lib/src/main/java/com/example/service/, for you:

package com.example.service;

import com.example.domain.Genre;
import com.example.repository.GenreRepository;
import jakarta.inject.Singleton;

import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;

public class GenreService {

    private final GenreRepository genreRepository;

    GenreService(GenreRepository genreRepository) {
        this.genreRepository = genreRepository;

    public Optional<Genre> findById(Long id) {
        return genreRepository.findById(id);

    public long update(long id, String name) {
        return genreRepository.update(id, name);

    public List<Genre> list(Pageable pageable) {
        return genreRepository.findAll(pageable).getContent();

    public Genre save(String name) {

    public void delete(long id) {

1.5. Tests #

The GCN Launcher wrote tests for you, in oci/src/test/java/com/example/, to verify the CRUD operations:

package com.example;

import com.example.domain.Genre;
import com.example.repository.GenreRepository;
import io.micronaut.core.type.Argument;
import io.micronaut.context.env.Environment;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

import static io.micronaut.http.HttpHeaders.LOCATION;
import static io.micronaut.http.HttpStatus.CREATED;
import static io.micronaut.http.HttpStatus.NOT_FOUND;
import static io.micronaut.http.HttpStatus.NO_CONTENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

@MicronautTest(environments = Environment.ORACLE_CLOUD)
class GenreControllerTest {

    HttpClient client;

    void testFindNonExistingGenreReturns404() {
        HttpClientResponseException thrown = assertThrows(HttpClientResponseException.class, () -> {

        assertEquals(NOT_FOUND, thrown.getStatus());

    void testGenreCrudOperations() {

        HttpResponse<?> response = client.toBlocking().exchange(
                HttpRequest.POST("/genres", Collections.singletonMap("name", "DevOps")));
        assertEquals(CREATED, response.getStatus());

        response = client.toBlocking().exchange(
                HttpRequest.POST("/genres", Collections.singletonMap("name", "Microservices")));
        assertEquals(CREATED, response.getStatus());

        Long id = entityId(response);

        Genre genre = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/" + id), Genre.class);
        assertEquals("Microservices", genre.getName());

        response = client.toBlocking().exchange(
                HttpRequest.PUT("/genres/" + id + "/Micro-services", null));
        assertEquals(NO_CONTENT, response.getStatus());

        genre = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/" + id), Genre.class);
        assertEquals("Micro-services", genre.getName());

        List<Genre> genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list"), Argument.listOf(Genre.class));
        assertEquals(2, genres.size());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1"), Argument.listOf(Genre.class));
        assertEquals(1, genres.size());
        assertEquals("DevOps", genres.get(0).getName());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1&sort=name,desc"), Argument.listOf(Genre.class));
        assertEquals(1, genres.size());
        assertEquals("Micro-services", genres.get(0).getName());

        genres = client.toBlocking().retrieve(
                HttpRequest.GET("/genres/list?size=1&page=2"), Argument.listOf(Genre.class));
        assertEquals(0, genres.size());

        response = client.toBlocking().exchange(
                HttpRequest.DELETE("/genres/" + id));
        assertEquals(NO_CONTENT, response.getStatus());

    private Long entityId(HttpResponse<?> response) {
        String value = response.header(LOCATION);
        if (value == null) {
            return null;
        String path = "/genres/";
        int index = value.indexOf(path);
        return index == -1 ? null : Long.valueOf(value.substring(index + path.length()));

    GenreRepository genreRepository;

    void cleanup() {

2. Provision an Oracle Autonomous Database #

  1. In the Oracle Cloud Console, open the navigation menu, click Oracle Database. Under Autonomous Database, click Autonomous Transaction Processing.

  2. Enter “GCNDemo” as the display name and the database name.

  3. Select Transaction Processing and Shared Infrastructure. If you are using a trial account, make sure you select Always Free.

  4. Create an admin password (must be at least 12 characters and contain a number and an uppercase letter) and select Secure access from everywhere as the network access type.

  5. Select License Included and click Create Autonomous Database to create your database.

  6. On the Autonomous Database Details screen click Copy to copy the OCID of the database. (This is a unique identifier for your database instance which you will need later.)

2.1. Create a User #

  1. On the Autonomous Database Details screen click Database Actions. Login with username “ADMIN” and the admin password you created earlier.

  2. Under Development click SQL to open the SQL console.

  3. Copy and paste the following SQL commands into the worksheet:


    Create a schema user password (must be at least 12 characters and contain a number and an uppercase letter) and replace the text “XXXXXXXXX” with that password.

  4. Click Run Script to run the SQL commands.

3. Configuration #

Before you can test or run your application, provide configuration for Flyway, the datasources, and optionally the IO Pool Size and logging.

3.1. Configure Flyway #

The GCN Launcher included Flyway for database migrations. It uses the Micronaut integration with Flyway that automates schema changes, significantly simplifies schema management tasks, such as migrating, rolling back, and reproducing in multiple environments. The GCN Launcher enables Flyway in the oci/src/main/resources/ file and configures it to perform migrations on the default datasources.


Note: Flyway migrations are not compatible with the default automatic schema generation that is configured in oci/src/main/resources/ If schema-generate is active, it will conflict with Flyway. So edit src/main/resources/ and either delete the datasources.default.schema-generate=CREATE_DROP line or change that line to datasources.default.schema-generate=NONE to ensure that only Flyway manages your schema.

Configuring multiple datasources is as simple as enabling Flyway for each one. You can also specify directories that will be used for migrating each data source. For more information, see Micronaut integration with Flyway.

Flyway migration is automatically triggered before your application starts. Flyway reads migration file(s) in the lib/src/main/resources/db/migration/ directory. The migration file with the database schema, lib/src/main/resources/db/migration/V1__schema.sql, was also created for you by the GCN Launcher.


During application startup, Flyway runs the commands in the SQL file and creates the schema needed for the application.

3.2. Configure Data Sources #

Oracle Cloud Autonomous Database connection information and credentials are stored in the Oracle Wallet. See the Micronaut Oracle Cloud integration documentation for more details and options for working with Oracle Cloud.

Set values for the missing datasources.default.ocid, datasources.default.walletPassword, datasources.default.username, and datasources.default.password properties by exporting them as environment variables as follows:

export DATASOURCES_DEFAULT_WALLET_PASSWORD=<wallet_password> # 2

Note: If you use Windows Command Prompt, replace export with set, for example, set DATASOURCES_DEFAULT_USERNAME=awsdb_user. If you use Windows PowerShell, replace export with $ and surround the value with double quote marks, for example, $DATASOURCES_DEFAULT_USERNAME="awsdb_user".

1 Replace the <ocid> with the database OCID unique identifier you saved when creating the database.

2 Replace the <wallet_password> with a password to encrypt the wallet keys (must be at least eight characters and include at least one letter and either one numeric or special character).

3 Replace the <username> with the gcndemo schema user username you created.

4 Replace the <password> with the gcndemo schema user password you created.

3.3. Configure IO Pool Size (Optional) #

It is a good idea to configure the IO pool size when using @ExecuteOn(TaskExecutors.IO) in controllers. Change this part of oci/src/main/resources/


3.4. Configure Logging (Optional) #

Edit oci/src/main/resources/logback.xml and add the following (anywhere within the <configuration> element) to monitor the SQL queries that Micronaut Data performs:

<logger name='' level='debug' />

4. Run the Tests (Optional) #

There are two options for running the tests: running against a live database, and running locally with an Oracle Database in a Docker container using Testcontainers.

Note: If your CPU architecture is AArch64, running the tests requires a production-like environment (a live database).

The GCN Launcher configured Flyway for the test environment in oci/test/resources/


Use the following command to run the tests:

./gradlew :oci:test
./mvnw install -pl lib -am && ./mvnw test -pl oci

Then open the file build/reports/tests/test/index.html in a browser to view the results.

5. Run the Application #

To run the application, use the following command, which starts the application on port 8080.

./gradlew :oci:run
./mvnw install -pl lib -am && ./mvnw mn:run -pl oci

6. Test the Application #

Test the application by accessing its REST endpoints.

Run this command to test creating and storing a new Genre in the database:

curl -X "POST" "http://localhost:8080/genres" \
        -H 'Content-Type: application/json; charset=utf-8' \
        -d $'{ "name": "music" }'

You should see output in the log and a response similar to:


Confirm that the new Genre is saved in the database by listing its contents:

curl localhost:8080/genres/list

You should see output in the log and a response similar to:


Delete the Genre you added and then list the contents of the database to confirm that it has been deleted:

curl -X "DELETE" "http://localhost:8080/genres/3"
curl localhost:8080/genres/list

7. Generate a Native Executable Using GraalVM #

GCN supports compiling a Java application ahead-of-time into a native executable using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

To generate a native executable, run the following command:

./gradlew :oci:nativeCompile

The native executable is created in the oci/build/native/nativeCompile/ directory and can be run with the following command:

./mvnw install -pl lib -am && ./mvnw package -pl oci -Dpackaging=native-image

The native executable is created in the oci/target/ directory and can be run with the following command:


8. Run and Test the Native Executable #

Run the native executable, and then perform the same tests as in step 6.

9. Clean up Cloud Resources #

When you finish using the database, you can terminate it from the Oracle Cloud Console. For more information, see Terminate an Autonomous Database Instance.

Summary #

This guide demonstrated how to use GCN to create a database application that stores data in an Oracle Cloud Infrastructure Autonomous Database using Micronaut Data. You also saw how to package this application into a native executable.