GraalVM native-image
Ekbatan native-image support is split across your build tool, your framework integration, and the ekbatan-native module. This page explains what each layer is responsible for and which settings users normally need.
Native-image support is not a different execution model for Ekbatan. Actions, repositories, optimistic locking, sharding, and outbox writes behave the same as on the JVM. The native-specific work is making GraalVM aware of reflection targets, SQL resources, programmatic Flyway migration resources, and the libraries used by the selected stack.
The Moving Parts
| Part | Responsibility |
|---|---|
| GraalVM JDK 25 | Provides the native-image compiler. |
| Stack native plugin | Spring Boot, Quarkus, Micronaut, or GraalVM Build Tools decide how the application binary/test binary is built. |
ekbatan-native | Registers Ekbatan/Jackson/jOOQ/Kafka/Avro/Testcontainers metadata. |
ekbatan-flyway | Optional module for FlywayMigrator, a programmatic Flyway runner for one datasource or every primary shard in ShardingConfig. |
| Scan package build arg | Tells Ekbatan’s native features where your application records, builders, events, and generated jOOQ classes live. |
| Resource inclusion | Ensures db/migration/*.sql and init scripts are bundled into the native image. |
Required App Configuration
Add ekbatan-native
Add the module to applications that build native binaries:
implementation("io.github.zyraz-io:ekbatan-native:0.2.1")
or Maven:
<dependency>
<groupId>io.github.zyraz-io</groupId>
<artifactId>ekbatan-native</artifactId>
<version>0.2.1</version>
</dependency>
If your app calls Ekbatan’s programmatic Flyway migrator, also add ekbatan-flyway:
implementation("io.github.zyraz-io:ekbatan-flyway:0.2.1")
<dependency>
<groupId>io.github.zyraz-io</groupId>
<artifactId>ekbatan-flyway</artifactId>
<version>0.2.1</version>
</dependency>
Set Scan Packages
The default scan root is io.ekbatan. Add your application root package at native-image build time:
| Consumer | Setting |
|---|---|
| Spring Boot / Micronaut / plain Gradle | graalvmNative.binaries.all { buildArgs.add("-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,com.your.package") } |
| Quarkus | quarkus.native.additional-build-args=-Dio.ekbatan.graalvm.scan.packages=io.ekbatan\,com.your.package |
| Maven native-maven-plugin | <buildArg>-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,com.your.package</buildArg> |
If this is missing, native runtime failures usually mention record components, action params records, event payload records, builder methods, or generated jOOQ classes.
Include SQL Resources
If Flyway runs from classpath migrations, include them in the image:
graalvmNative {
binaries.all {
resources.includedPatterns.add("db/migration/.*\\.sql")
resources.includedPatterns.add(".*_init\\.sql")
}
}
Maven equivalent:
<buildArg>-H:IncludeResources=db/migration/.*\.sql</buildArg>
Use a different pattern if your app stores migrations somewhere else.
Use A Native-Image-Capable Toolchain
Gradle native examples require Java 25 and nativeImageCapable.set(true), not a hard-coded GraalVM vendor. That works locally with SDKMAN/asdf/system GraalVM installs and also works in CI with actions/setup-java using distribution: graalvm.
If Gradle chooses the wrong Java 25 installation, set JAVA_HOME to the GraalVM JDK or pin the build with:
./gradlew -Dorg.gradle.java.installations.paths="$JAVA_HOME" \
-Dorg.gradle.java.installations.auto-detect=false \
nativeTest
What ekbatan-native Auto-Loads
Each feature is registered through META-INF/native-image/.../native-image.properties. Features detect their target libraries and no-op when the library is absent.
| Feature | Triggers when classpath contains | Registers |
|---|---|---|
Jackson3RecordsFeature | Always | Java records, @AutoBuilder builder classes, classes with @JsonCreator, and classes in .generated.jooq. packages. |
KafkaClientsFeature | org.apache.kafka.clients.consumer.KafkaConsumer | Kafka security and serialization packages plus default partitioners/assignors referenced by Kafka config strings. |
AvroSpecificRecordFeature | org.apache.avro.specific.SpecificRecord | Avro SpecificRecord implementations under the configured Avro scan packages. |
TestcontainersDockerJavaFeature | org.testcontainers.DockerClientFactory | docker-java API/model/command packages, shaded and unshaded. |
ekbatan-native also bundles HikariCP reachability metadata. The jOOQ native substitution for Internal.arrayType(...) lives in ekbatan-core and is applied automatically.
Jackson 3 Records And Builders
Ekbatan serializes events with Jackson 3 (tools.jackson.databind.*). Jackson needs reflection metadata for records, action params, event payloads, generated builders, and some value factories. Jackson3RecordsFeature scans the configured packages and registers both:
- bulk query metadata, so reflection queries such as
getDeclaredMethods()work; and - per-member invocation metadata, so Jackson can actually invoke constructors, methods, and record accessors at runtime.
Both are needed on GraalVM 25.
Flyway On Native
Flyway’s normal classpath scanner does not always work inside a native image because classpath resources are not exposed as normal file: or jar: directories. Ekbatan supports two patterns, and the examples use them differently by stack.
| Stack | Recommended pattern |
|---|---|
| Spring Boot | Keep spring-boot-starter-flyway on the classpath and call FlywayMigrator.migrate(shardingConfig) from a startup bean. The Ekbatan Spring starter filters Boot’s default single-datasource DataSource/Flyway auto-configuration. |
| Quarkus | Use ekbatan-flyway from a StartupEvent observer that calls FlywayMigrator.migrate(shardingConfig). Keep quarkus-flyway and the matching quarkus-jdbc-* extension on the classpath for Flyway/driver native-image integration. |
| Micronaut | The native examples use a small startup migrator that calls FlywayMigrator.migrate(...). They keep micronaut-flyway on the classpath for Flyway/native dependencies and hints, but do not use a flyway: auto-config block. |
| Plain Java / raw tests | Use FlywayMigrator.migrate(...) directly. |
FlywayMigrator is a wrapper around normal Flyway configuration. On the JVM it behaves like inline Flyway.configure().dataSource(...).locations(...).load().migrate(). In a native image it installs an internal resource scanner that can walk bundled classpath: migrations.
import io.ekbatan.flyway.FlywayMigrator;
FlywayMigrator.migrate(jdbcUrl, username, password);
FlywayMigrator.migrate(jdbcUrl, username, password, "classpath:db/migration", "classpath:db/seed");
FlywayMigrator.migrate(shardingConfig); // runs on every primary shard, sequentially
Build And Test Commands
The exact command depends on the stack:
| Stack/build | Build native app | Native verification |
|---|---|---|
| Spring Boot Gradle | ./gradlew nativeCompile | ./gradlew nativeTest |
| Spring Boot Maven | ./mvnw -Pnative native:compile | ./mvnw -PnativeTest test |
| Quarkus Gradle | ./gradlew build -Dquarkus.native.enabled=true | ./gradlew testNative |
| Quarkus Maven | ./mvnw -Dnative package | ./mvnw -Dnative verify |
| Micronaut Gradle | ./gradlew nativeCompile | ./gradlew nativeTest |
| Micronaut Maven | ./mvnw -Dpackaging=native-image -DskipTests package | Native tests are not enabled in the Maven Micronaut examples. |
Ekbatan’s Heavy Verification workflow runs JVM tests plus native builds/tests for the examples. The root Gradle native sweep uses:
./gradlew nativeTest --parallel --max-workers=4 --continue --stacktrace
Use that as heavy verification, not as the normal edit-compile-test loop.
Troubleshooting
native-imageis missing or Gradle selects Temurin/OpenJDK. Use a Java 25 GraalVM JDK and make sure Gradle sees thenative-imagecapable installation.- Jackson cannot deserialize action params or events. Add your application package to
io.ekbatan.graalvm.scan.packages. - Flyway sees no migrations. Include
db/migration/*.sqlas native resources and use the Flyway pattern for your stack. - Quarkus and HikariCP. Quarkus prefers Agroal and does not consume all generic RMR metadata automatically. Depending on
ekbatan-nativeis the simplest way to bring Ekbatan’s HikariCP metadata into the native classpath. - Micronaut native tests and Hikari DEBUG logging. The Micronaut native examples ship a minimal
logback.xmlthat keeps Hikari below DEBUG, avoiding reflection over every JavaBean property during startup.
See Also
- Learn -> Compiling to a native binary - tutorial-level walkthrough
- Wiring with Spring Boot - Spring AOT and Flyway details
- Wiring with Quarkus - Quarkus native and Flyway details
- Wiring with Micronaut - Micronaut native and serde details
ekbatan-examples/*-native-*- runnable native wallet examples