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:
- Application types — action params, event payloads, records, builders, and generated jOOQ classes that Jackson or the framework may need to construct reflectively.
- SQL resources — Flyway migrations and init SQL files that must be bundled into the native executable.
- Your stack’s native build pipeline — Spring Boot, Quarkus, Micronaut, Gradle, and Maven use different native build plugins and command names.
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
- Use a Java 25 GraalVM JDK.
- Start from the matching
*-native-*wallet example if possible. - Add
io.github.zyraz-io:ekbatan-native. - Tell native-image which application packages to scan.
- Make migrations work in the native binary: run Flyway through
FlywayMigratorand include SQL resources. - 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:
- Jackson 3 reflection metadata for Java records, action params, event payloads,
@JsonCreatorclasses,@AutoBuilderbuilders, and generated jOOQ classes. - GraalVM metadata for Kafka clients, Avro
SpecificRecords, and Testcontainers/docker-java when those libraries are present. - HikariCP reachability metadata.
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.exampleQuarkus 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:
- The migration SQL files must be bundled into the native executable.
- 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:
| Stack | Migration pattern in the native examples |
|---|---|
| Spring Boot | spring-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. |
| Quarkus | A 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. |
| Micronaut | A 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 Flyway | Call 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 packageThe 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
- The business code did not change. Native-image did not alter
ActionExecutor, repository routing, optimistic locking, or event persistence. ekbatan-nativeregistered the reflection metadata that Ekbatan’s Jackson 3 event serialization and generated builders need.- Your build included SQL resources so Flyway can still find migrations after the app is compiled into a binary.
- Native verification moved to the slower path. This is why Ekbatan has a separate Heavy Verification workflow instead of running every native test after every push.
Where This Can Bite
- Wrong JDK selected. The build must use a Java 25 GraalVM JDK with
native-image. - Missing scan package. If native runtime cannot deserialize your params/events, check
io.ekbatan.graalvm.scan.packages. - Missing SQL resources. If Flyway finds no migrations on native, check resource inclusion.
- Stack-specific plugins. Spring, Quarkus, and Micronaut each have their own native pipeline. Copy the matching example instead of mixing snippets across stacks.
See Also
- Reference -> GraalVM native-image - feature list, Flyway patterns, toolchain notes, troubleshooting
ekbatan-examples/*-native-*- 19 native wallet examples across Spring Boot, Quarkus, Micronaut, Gradle, Maven, the three supported databases, and the sharded Spring Boot PG example- GraalVM Build Tools docs - official Gradle/Maven native plugin reference
- Quarkus native guide - Quarkus-specific native build details