Project setup

Apply StoatFlow's build conventions with Gradle or Maven — the JDK 25 toolchain, the required JVM flags, a runnable fat JAR, and opt-in reproducible Docker and GraalVM native images, from a few lines of config.

A StoatFlow application has non-trivial build requirements: a JDK 25 toolchain, two mandatory JVM flags at compile, test, and run time, a fat JAR with the right Main-Class, and — when you opt in — a container-aware heap entrypoint and a GraalVM native image with the correct metadata. You don't hand-maintain any of that. StoatFlow ships first-class build conventions for both Gradle and Maven, kept deliberately symmetric, so a production-ready build is a few lines of config.

Apply the conventions

Register the StoatFlow repository (and, for Gradle, the plugin-marker repository) as shown in Installation. Then apply the convention:

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.4.0"      // omit for a Java-only project
    id("io.stoatflow") version "<stoatflow-version>"
}

stoatflow {
    mainClass.set("com.example.MainKt") // your application entry point
}

dependencies {
    implementation("io.stoatflow:stoatflow-runtime:<stoatflow-version>")
}

The current published version is 1.0.0-alpha.16 — substitute it for the placeholders above (the Maven <parent> version must be a literal; Maven does not interpolate properties in parent coordinates). For a Kotlin fun main() in Main.kt the entry point is the synthetic MainKt class (com.example.MainKt); for a Java public static void main it's the class itself (com.example.Main).

The Maven parent imports stoatflow-bom for you, so you omit the <version> on every io.stoatflow:* dependency. If you already have a corporate parent POM, import the BOM directly instead and set the toolchain by hand — see Maven reference → Using just the BOM.

What you get

Applying the convention — with no further configuration — already gives you all of this:

CapabilityWhat it does
JDK 25 toolchainCompiles and runs on JDK 25 — required for the virtual-thread / structured-concurrency preview features StoatFlow is built on.
Mandatory JVM flags--enable-preview and --enable-native-access=ALL-UNNAMED at compile, test, and run time. Non-negotiable and always added.
Default GC flag-XX:+UseG1GC — overridable, but on by default for predictable pauses even on a single core or a small container.
Runnable fat JARA shaded -all JAR with the Main-Class manifest and merged service files. java -jar just works.
Test conventionsJUnit Platform with the preview/native-access flags; an integrationTest task that runs only @Tag("IntegrationTest") tests (the unit run excludes them).
Docker image (opt-in)A reproducible JVM container via Jib — no Dockerfile, no Docker daemon to assemble the layers. See Docker.
Native image (opt-in)A GraalVM native image with all of StoatFlow's reflection / JNI / FFM metadata already registered. One line to turn on. See Native image.
-XX:+UseG1GC is set explicitly because the JVM only auto-selects G1 on "server-class" hardware (≥ 2 CPUs and ≥ ~2 GB memory), falling back to the single-threaded Serial collector below that. Setting it explicitly gives short, predictable GC pauses even on a single core — which matters for stream processing, where a long stop-the-world pause stalls commit barriers. See Installation for the full reasoning.

Configure the build

mainClass is the only property most projects set. The container and native-image builds are opt-in and default to off. Here are the common knobs — the full surface (every property, task, and default) lives in the reference pages, linked below.

// build.gradle.kts
stoatflow {
    mainClass.set("com.example.MainKt")

    docker {
        enabled.set(true)               // opt in to the Jib image
        imageName.set("acme/word-count")
    }

    nativeImage {
        enabled.set(true)               // opt in to the GraalVM native image
        gc.set("G1")                    // Oracle GraalVM; omit for Serial (stateless only)
    }
}
Maven profiles are activated with -P, not <properties>. Setting <stoatflow.docker.enabled> does not turn the build on — Maven <activation> reads system/user properties, not your project's. Activate the build with mvn -Pstoatflow-docker … / mvn -Pstoatflow-native … (the build commands below). The <properties> above only configure the build once a profile is active.
  • Full Gradle DSL — every stoatflow { } property, task, and default: Gradle plugin reference.
  • Full Maven surface — the three artifacts, every <properties> key, and the four goals: Maven reference.

Gradle ⇄ Maven: the same conventions, both tools

Everything one tool does has a counterpart on the other. If you've configured one, this is the map to the other:

ConceptGradle (io.stoatflow plugin)Maven (stoatflow-parent + plugin)
Apply the conventionsplugins { id("io.stoatflow") }<parent>io.stoatflow:stoatflow-parent</parent>
Entry pointstoatflow { mainClass.set("…") }<stoatflow.mainClass>…</stoatflow.mainClass>
Fat JAR./gradlew shadowJar (-all.jar)mvn package (-all.jar, via shade)
Run (forked, preview on)./gradlew runmvn stoatflow:run
Build a Docker imagedocker { enabled.set(true) }./gradlew jibDockerBuild<stoatflow.docker…>mvn -Pstoatflow-docker package
Build a native imagenativeImage { enabled.set(true) }./gradlew nativeDockerBuild<stoatflow.nativeImage…>mvn stoatflow:native-docker-build
Aligned dependency versionsresolved transitively from stoatflow-runtimeimport stoatflow-bom (the parent does this for you)
One source of truth under the hood. Both build tools bundle the same native-image argument file, the same generic Dockerfile.native, and the same container entrypoint scripts (shared from one internal module). A change to the native flags or the heap model lands on both at once — they can't drift.

Shared build behaviours

These are identical across Gradle and Maven — worth understanding once.

  • Reproducible images. By default the image creationTime (JVM/Jib) and SOURCE_DATE_EPOCH (native) are pinned to your HEAD commit timestamp, not wall-clock — so identical source produces an identical digest with a meaningful Created date. Outside a git repo the timestamp falls back to the Unix epoch. Turn it off with reproducibleBuild = false to restore wall-clock.
  • Container-aware heap sizing. The bundled entrypoint sizes the JVM (or native) heap from the container's memory limit (cgroup v1/v2), not the host's — no -Xmx guesswork under Kubernetes limits. The full model lives in Docker → Container-aware heap sizing.
  • RocksDB off-heap auto-detection. If org.rocksdb is on the runtime classpath (it is, via stoatflow-runtime/stoatflow-core), the image sets STOATFLOW_ROCKSDB_MB=256 so the entrypoint reserves off-heap memory for the state store before sizing the heap. Override the reservation explicitly, or set it to 0 to skip the reservation for an app that only uses in-memory stores. Automatic on both build tools.

Build and run

./gradlew run             # run locally with the required JVM flags
./gradlew shadowJar       # build the fat JAR  → build/libs/<name>-<version>-all.jar
./gradlew test            # unit tests (IntegrationTest tag excluded)
./gradlew integrationTest # integration tests (only the IntegrationTest tag)
./gradlew jibDockerBuild  # build the container image   (docker.enabled)
./gradlew nativeDockerBuild # build the native image      (nativeImage.enabled)
mvn stoatflow:… short prefix. To call the plugin's standalone goals by their short prefix, add io.stoatflow to <pluginGroups> in your ~/.m2/settings.xml. Without it, use full coordinates — mvn io.stoatflow:stoatflow-maven-plugin:run. See the Maven reference.

Worked examples

A complete build for each tool, both languages:

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.4.0"   // drop this line for a Java-only project
    id("io.stoatflow") version "<stoatflow-version>"
}

dependencies {
    implementation("io.stoatflow:stoatflow-runtime:<stoatflow-version>")
    testImplementation("io.stoatflow:stoatflow-test-utils:<stoatflow-version>")
}

stoatflow {
    mainClass.set("com.example.MainKt")   // Java: "com.example.Main"
    docker {
        enabled.set(true)
        imageName.set("acme/word-count")
    }
}
Don't want a build plugin at all? Both tools have a "configure it yourself" path — set the JDK 25 toolchain and the two JVM flags by hand. See Installation → Option B.

Next steps