§ learn

Compiling to a native binary

Build the same Ekbatan wallet app as a GraalVM native executable. Your domain model, Actions, repositories, event rows, and handlers stay the same. Native-image only changes packaging and build-time metadata.

A GraalVM native image is an ahead-of-time compiled executable for your JVM app. You still write normal Java, but the build turns your application, dependencies, and enough of the JDK into one binary. Startup is much faster and there is no JVM to install on the target machine, but the build is slower and native-image must know up front about reflection, resources, and dynamic library behavior.

For Ekbatan users, the important point is simple: native-image does not change Ekbatan’s persistence model. Your action still commits domain rows and eventlog.events rows in one transaction. Native-image only changes how the application is packaged and how reflection/resource metadata is supplied to GraalVM.

Feel free to skip this lesson if you do not need native builds or native-image packaging. Ekbatan works normally on the JVM, and the rest of the learning path does not depend on this page.

What Native-Image Needs To Know

Native-image freezes more things at build time than a normal JVM run. If code discovers something dynamically at runtime, the native build may need a hint ahead of time.

For an Ekbatan application, there are three important categories:

The sections below add those pieces one by one.

Choose Your Stack

Pick the stack, build tool, and database you are using. Most native-image settings are shared across PostgreSQL, MariaDB, and MySQL, but the example row points to the matching native project.

The Short Version

  1. Use a Java 25 GraalVM JDK.
  2. Start from the matching *-native-* wallet example if possible.
  3. Add io.github.zyraz-io:ekbatan-native.
  4. Tell native-image which application packages to scan.
  5. Make migrations work in the native binary: run Flyway through FlywayMigrator and include SQL resources.
  6. Run JVM tests frequently, and run native tests/builds in heavier verification.

1. Install GraalVM

sdk install java 25.0.3-graal
sdk use java 25.0.3-graal

java -version
native-image --version

Gradle examples use a Java 25 toolchain that is nativeImageCapable, so SDKMAN, asdf, and normal system installs usually work without hard-coding a vendor. If your machine has multiple Java 25 installations and Gradle chooses a non-GraalVM JDK, set JAVA_HOME to the GraalVM install before running native tasks.

2. Start From The Native Example

The native examples are the cleanest source of truth because they include the stack-specific build plugin, resource inclusion, scan package settings, and tests.

Use spring-boot-wallet-rest-gradle-native-{pg,mariadb,mysql}. It is the same Spring Boot wallet app as the JVM sibling, with the GraalVM Build Tools plugin and native metadata added.

Use spring-boot-wallet-rest-maven-native-{pg,mariadb,mysql}. The Maven native examples keep the same Spring Boot wiring and add the native-maven-plugin configuration.

Use quarkus-wallet-rest-gradle-native-{pg,mariadb,mysql}. Quarkus owns the native build pipeline; Ekbatan mainly contributes its native metadata and scan-package setting.

Use quarkus-wallet-rest-maven-native-{pg,mariadb,mysql}. The native profile builds the Quarkus runner and runs the integration test against that binary.

Use micronaut-wallet-rest-gradle-native-{pg,mariadb,mysql}. Micronaut’s application plugin wires the GraalVM plugin; the example adds Ekbatan native metadata and resource inclusion.

Use micronaut-wallet-rest-maven-native-{pg,mariadb,mysql}. These examples build a native executable with -Dpackaging=native-image; native test execution is not enabled in the Maven Micronaut examples.

There is no scaffolded plain-Java native wallet example. Use Plain Java wiring for the application shape and Reference -> GraalVM native-image for the native configuration.

3. Add ekbatan-native

Add this dependency only to modules that build native binaries:

dependencies {
    implementation("io.github.zyraz-io:ekbatan-native:0.2.1")
}
<dependency>
  <groupId>io.github.zyraz-io</groupId>
  <artifactId>ekbatan-native</artifactId>
  <version>0.2.1</version>
</dependency>

ekbatan-native gives native-image the metadata Ekbatan commonly needs:

The jOOQ native substitution lives in ekbatan-core, so it is available without extra user code.

If your app calls Ekbatan’s programmatic Flyway migrator, also add ekbatan-flyway. That module provides FlywayMigrator, which can run one datasource or every primary shard in ShardingConfig.

4. Set The Scan Package

Ekbatan can register its own framework classes automatically, but it cannot know your application package unless you tell it. Add your root package to the native-image build args:

graalvmNative {
    binaries.all {
        buildArgs.add("-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,io.example")
    }
}
graalvmNative {
    binaries.all {
        buildArgs.add("-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,io.example")
    }
}
quarkus.native.additional-build-args=-Dio.ekbatan.graalvm.scan.packages=io.ekbatan\,io.example

Quarkus uses commas to separate build args, so escape the comma inside the property value.

<buildArgs combine.children="append">
  <buildArg>-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,io.example</buildArg>
</buildArgs>
<buildArgs combine.children="append">
  <buildArg>-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,io.example</buildArg>
</buildArgs>

combine.children="append" matters for Micronaut Maven because the parent/native plugin already contributes required native build args.

If you forget this, the usual failure is a native runtime error around missing record reflection, often for an action params record or event payload record.

5. Make Migrations Work In The Native Binary

There are two separate requirements:

  1. The migration SQL files must be bundled into the native executable.
  2. Flyway should run through FlywayMigrator.migrate(shardingConfig), so the same migrations are applied to every configured shard.

Spring Boot, Quarkus, and Micronaut can still keep their Flyway integration dependencies. Those dependencies provide Flyway and stack-specific native-image support. Ekbatan’s startup migrator decides which shard datasources Flyway runs against.

Use the pattern your stack example uses:

StackMigration pattern in the native examples
Spring Bootspring-boot-starter-flyway stays on the classpath, a startup bean calls FlywayMigrator.migrate(shardingConfig), and the Ekbatan Spring starter filters Boot’s default single-datasource DataSource/Flyway auto-configuration.
QuarkusA StartupEvent observer calls FlywayMigrator.migrate(shardingConfig) from ekbatan-flyway; quarkus-flyway and the matching quarkus-jdbc-* extension stay on the classpath for Flyway/driver native-image integration.
MicronautA small startup migrator calls FlywayMigrator.migrate(...); micronaut-flyway remains on the classpath for Flyway/native support, but the example does not use a flyway: auto-config block.
Plain Java / raw FlywayCall FlywayMigrator.migrate(...) yourself.

FlywayMigrator is a thin wrapper. On the JVM it behaves like normal Flyway.configure().dataSource(...).load().migrate(). In a native image it installs Ekbatan’s internal resource scanner so Flyway can find classpath:db/migration.

Native-image only bundles resources that are explicitly included. If your app runs Flyway from classpath SQL files, include the migrations in the native build:

graalvmNative {
    binaries.all {
        resources.includedPatterns.add("db/migration/.*\\.sql")
        resources.includedPatterns.add(".*_init\\.sql")
    }
}
<buildArgs combine.children="append">
  <buildArg>-H:IncludeResources=db/migration/.*\.sql</buildArg>
</buildArgs>

If migrations live somewhere else, add that path instead.

6. Build And Verify

Use JVM tests for normal development. Native builds are slower and should be treated as pre-release or heavier CI verification.

./gradlew test
./gradlew nativeCompile
./gradlew nativeTest
./mvnw verify
./mvnw -Pnative native:compile
./mvnw -PnativeTest test
./gradlew test
./gradlew build -Dquarkus.native.enabled=true
./gradlew testNative
./mvnw verify
./mvnw -Dnative verify
./gradlew test
./gradlew nativeCompile
./gradlew nativeTest
./mvnw verify
./mvnw -Dpackaging=native-image -DskipTests package

The Maven Micronaut native examples build the native executable but do not enable native test execution.

Use the GraalVM Build Tools nativeCompile / nativeTest tasks or native-maven-plugin directly. The exact shape depends on how you wire your plain Java app.

What Just Happened

Where This Can Bite

See Also