# GraalVM native-image
Ekbatan native-image support is split across your build tool, your framework integration, the `ekbatan-native` module, and optionally `ekbatan-flyway` when you run Flyway programmatically. 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:
```kotlin
implementation("io.github.zyraz-io:ekbatan-native:0.2.1")
```
or Maven:
```xml
io.github.zyraz-io
ekbatan-native
0.2.1
```
If your app calls Ekbatan's programmatic Flyway migrator, also add `ekbatan-flyway`:
```kotlin
implementation("io.github.zyraz-io:ekbatan-flyway:0.2.1")
```
```xml
io.github.zyraz-io
ekbatan-flyway
0.2.1
```
### 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 | `-Dio.ekbatan.graalvm.scan.packages=io.ekbatan,com.your.package` |
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:
```kotlin
graalvmNative {
binaries.all {
resources.includedPatterns.add("db/migration/.*\\.sql")
resources.includedPatterns.add(".*_init\\.sql")
}
}
```
Maven equivalent:
```xml
-H:IncludeResources=db/migration/.*\.sql
```
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:
```bash
./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.
```java
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:
```bash
./gradlew nativeTest --parallel --max-workers=4 --continue --stacktrace
```
Use that as heavy verification, not as the normal edit-compile-test loop.
## Troubleshooting
- **`native-image` is missing or Gradle selects Temurin/OpenJDK.** Use a Java 25 GraalVM JDK and make sure Gradle sees the `native-image` capable 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/*.sql` as 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-native` is 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.xml` that keeps Hikari below DEBUG, avoiding reflection over every JavaBean property during startup.
## See also
- [Wiring with Spring Boot](../wiring/spring.md) - Spring AOT and Flyway details
- [Wiring with Quarkus](../wiring/quarkus.md) - Quarkus native and Flyway details
- [Wiring with Micronaut](../wiring/micronaut.md) - Micronaut native and serde details
- [`ekbatan-examples/*-native-*`](../../ekbatan-examples) - runnable native wallet examples