§ reference

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

PartResponsibility
GraalVM JDK 25Provides the native-image compiler.
Stack native pluginSpring Boot, Quarkus, Micronaut, or GraalVM Build Tools decide how the application binary/test binary is built.
ekbatan-nativeRegisters Ekbatan/Jackson/jOOQ/Kafka/Avro/Testcontainers metadata.
ekbatan-flywayOptional module for FlywayMigrator, a programmatic Flyway runner for one datasource or every primary shard in ShardingConfig.
Scan package build argTells Ekbatan’s native features where your application records, builders, events, and generated jOOQ classes live.
Resource inclusionEnsures 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:

ConsumerSetting
Spring Boot / Micronaut / plain GradlegraalvmNative.binaries.all { buildArgs.add("-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,com.your.package") }
Quarkusquarkus.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.

FeatureTriggers when classpath containsRegisters
Jackson3RecordsFeatureAlwaysJava records, @AutoBuilder builder classes, classes with @JsonCreator, and classes in .generated.jooq. packages.
KafkaClientsFeatureorg.apache.kafka.clients.consumer.KafkaConsumerKafka security and serialization packages plus default partitioners/assignors referenced by Kafka config strings.
AvroSpecificRecordFeatureorg.apache.avro.specific.SpecificRecordAvro SpecificRecord implementations under the configured Avro scan packages.
TestcontainersDockerJavaFeatureorg.testcontainers.DockerClientFactorydocker-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:

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.

StackRecommended pattern
Spring BootKeep 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.
QuarkusUse 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.
MicronautThe 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 testsUse 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/buildBuild native appNative 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 packageNative 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

See Also