Project and build
Kora is a cloud-oriented server framework written in Java for applications written in Java and Kotlin.
This page describes the basic principles of Kora, environment requirements, annotation processor setup, minimal Gradle configuration, dependency management, and application startup.
Kora provides a set of modules for quickly building server applications: HTTP server and HTTP client, Kafka consumers, repositories for working with databases, S3 client, gRPC server and gRPC client, Camunda integrations, module telemetry, resilience, and other capabilities.
The main framework characteristics are described on the home page.
Kora provides the tools usually needed for modern server-side development:
- dependency injection through annotations;
- inversion of control without a separate container at runtime;
- aspect-oriented programming through annotations;
- sufficiently high-level simple abstractions and development tools;
- a large set of preconfigured integrations;
- telemetry, tracing, metrics according to the
OpenTelemetrystandard, and module logging; - fast testing with JUnit5;
- working examples and guides.
For high-performance, efficient, and predictable code, Kora follows these principles:
- does not use
Reflectionduring application runtime; - does not use
dynamic proxyduring application runtime; - does not generate bytecode during compilation or application runtime;
- creates source code at compile time through annotation processors;
- keeps thin abstractions over integrations;
- provides free aspects: without additional cost during application runtime;
- uses only the most efficient implementations for integrations;
- encourages and uses the most effective development principles and natural language constructs.
Kora executes application code synchronously on virtual threads.
Controllers, HTTP clients, repositories, and scheduled tasks are declared with ordinary blocking signatures, and the framework dispatches them onto virtual threads itself: an HTTP request, for example, is handled on a virtual thread bound to its connection.
There are no reactive or suspend contracts in Kora modules — the processors reject suspend controller, client, repository, and scheduler methods with a compilation error.
When a single operation needs to do several things in parallel, use StructuredTaskScope from Java structured concurrency; it is a preview API, so both compilation and every JVM launch require --enable-preview.
If you need a step-by-step walkthrough before the reference description, see Creating Your First Kora Application and Dependency Injection Introduction.
Annotation Processors¶
Kora builds the application at compile time: processors read annotations, validate the code, and generate source files that are then compiled together with the application code.
As a result, the dependency graph, aspects, HTTP handlers, repositories, and other components become ordinary compiled code without Reflection at runtime.
An annotation is a construct associated with Java source code elements: classes, methods, parameters, and fields.
An annotation processor is started by the compiler, reads these annotations, and can generate additional source code or stop compilation with a clear error.
Kora provides all annotation processors in a single dependency:
This dependency is needed only at compile time and does not add extra libraries to the application runtime classpath.
KSP (Kotlin Symbol Processing) is used for Kotlin.
KSP reads Kotlin source code symbols, passes them to Kora processors, and allows code generation before the main compilation step.
Kora provides KSP processors in a single dependency:
At the same time, Kotlin processing is usually slower than annotation processing in Java.
KSP¶
KSP is needed only for Kotlin projects.
If the application is written in Java, use the regular annotationProcessor; if the application is written in Kotlin, connect com.google.devtools.ksp and the io.koraframework:symbol-processors dependency.
KSP writes generated sources into build/generated/ksp/main/kotlin and build/generated/ksp/test/kotlin.
The KSP Gradle plugin adds those directories to compilation itself; the build files on this page also declare them explicitly in the source sets:
kotlin {
sourceSets.main { kotlin.srcDir("build/generated/ksp/main/kotlin") }
sourceSets.test { kotlin.srcDir("build/generated/ksp/test/kotlin") }
}
If another task has to run before code generation (for example OpenAPI or protobuf generation), bind it to the KSP tasks by name.
KSP 2 no longer exposes the KspTask type, so tasks.withType<KspTask>() is not available:
Compatibility¶
Kora artifacts are compiled and published for Java 25: both the Java and the Kotlin parts of the framework are built with sourceCompatibility/targetCompatibility 25 and jvmTarget 25, and the published BOM declares java.version 25.
So JDK 25 is the minimum for compiling and running an application on Kora, regardless of the language.
Requires at least JDK 25, it is recommended to use the latest available GA release of the JDK.
Minimal configuration in build.gradle:
plugins {
id "java"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.ADOPTIUM
}
}
The vendor pin is optional and simply matches the Adoptium toolchain used by the example projects; you may omit it or select another vendor.
Requires at least JDK 25, it is recommended to use the latest available GA release of the JDK.
Use the same versions the framework itself is built with: Kotlin 2.4.10 and KSP 2.3.11.
A mismatch between the Kotlin compiler and the compiler embedded in KSP leads to symbol processor failures that are hard to diagnose, so both versions are pinned together.
Minimal configuration in build.gradle.kts:
plugins {
kotlin("jvm") version "2.4.10"
id("com.google.devtools.ksp") version "2.3.11"
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(25))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
sourceSets.main { kotlin.srcDir("build/generated/ksp/main/kotlin") }
sourceSets.test { kotlin.srcDir("build/generated/ksp/test/kotlin") }
}
The Gradle process itself also needs a new enough JDK
A Gradle toolchain only affects compilation and application launch; the buildscript classpath is resolved by the JVM that runs Gradle.
As soon as a Kora artifact lands there — the most common case is io.koraframework:openapi-generator for OpenAPI code generation — an older Gradle JVM fails at the configuration stage:
Start Gradle on JDK 25+ through JAVA_HOME or through the Gradle daemon JVM settings.
Do not hardcode org.gradle.java.home in the repository gradle.properties: the path is machine-specific.
Toolchain auto-provisioning
So that Gradle can download a missing JDK for the toolchain by itself, Kora example projects register the resolver in settings.gradle:
and enable downloading in gradle.properties:
Nullability
In Java, Kora marks nullability with JSpecify — org.jspecify.annotations.Nullable, which comes transitively with any Kora module.
These are type-use annotations, so their position matters: Outer.@Nullable Inner, List<@Nullable String>, String @Nullable [].
In Kotlin nullability is expressed by the type itself (T?) and no annotation is needed; when overriding a Kora contract whose parameter is marked @Nullable, declare the parameter as nullable.
Build System¶
Kora is designed to be built with Gradle because Gradle has good support for annotation processors, KSP, incremental builds, and dependency management.
The framework itself and all Kora example projects are built with Gradle 9.5.1, so Gradle 9.5+ is the recommended version.
To avoid specifying versions for each Kora dependency separately, use the BOM io.koraframework:kora-bom.
The BOM version is specified once, and the rest of the Kora dependencies are declared without an explicit version.
Minimal application configuration in build.gradle:
plugins {
id "java"
id "application"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.ADOPTIUM
}
}
configurations {
koraBom //(1)!
annotationProcessor.extendsFrom(koraBom)
implementation.extendsFrom(koraBom)
testAnnotationProcessor.extendsFrom(koraBom)
}
dependencies {
koraBom platform("io.koraframework:kora-bom:2.0.0.RC1")
annotationProcessor "io.koraframework:annotation-processors"
implementation "io.koraframework:config-hocon"
implementation "io.koraframework:http-server-undertow"
implementation "io.koraframework:json-common"
implementation "io.koraframework:logging-logback"
}
- A separate
koraBomconfiguration that all the others extend.platform()applied only toimplementationwould not reachannotationProcessorandtestAnnotationProcessor, and the processor dependency would then have to carry an explicit version.
A more detailed example is available in Creating Your First Kora Application.
Gradle Kotlin DSL is assumed for Kotlin.
If the project uses Groovy DSL, follow the Java examples.
Minimal application configuration in build.gradle.kts:
plugins {
id("application")
kotlin("jvm") version "2.4.10"
id("com.google.devtools.ksp") version "2.3.11"
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(25))
vendor.set(JvmVendorSpec.ADOPTIUM)
}
sourceSets.main { kotlin.srcDir("build/generated/ksp/main/kotlin") }
sourceSets.test { kotlin.srcDir("build/generated/ksp/test/kotlin") }
}
dependencies {
implementation(platform("io.koraframework:kora-bom:2.0.0.RC1")) //(1)!
ksp("io.koraframework:symbol-processors:2.0.0.RC1") //(2)!
implementation("io.koraframework:config-hocon")
implementation("io.koraframework:http-server-undertow")
implementation("io.koraframework:json-common")
implementation("io.koraframework:logging-logback")
}
- In
Kotlinprojects theBOMis applied directly toimplementation; a separatekoraBomconfiguration withextendsFromis not created. - The
kspconfiguration is not covered by theBOM, so the processor version is specified explicitly.
A more detailed example is available in Creating Your First Kora Application.
In real projects the BOM version is usually extracted into a gradle.properties property (for example koraVersion) and referenced as platform("io.koraframework:kora-bom:$koraVersion"), so the version is declared in a single place instead of being hardcoded in every module.
Compiler internal access
Some Java annotation processors read jdk.compiler internals. On newer JDKs this may require exporting the corresponding packages to the compiler.
All Kora example projects set these JVM arguments in gradle.properties unconditionally; add them if compilation fails with IllegalAccessError or module jdk.compiler does not export ...:
org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED \
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
Dependencies¶
Kora module documentation usually shows only the dependency of a specific module.
But the application must also connect the BOM and processors shown below.
build.gradle:
configurations {
koraBom
annotationProcessor.extendsFrom(koraBom)
implementation.extendsFrom(koraBom)
testAnnotationProcessor.extendsFrom(koraBom)
}
dependencies {
koraBom platform("io.koraframework:kora-bom:2.0.0.RC1") //(1)!
annotationProcessor "io.koraframework:annotation-processors" //(2)!
testAnnotationProcessor "io.koraframework:annotation-processors" //(3)!
}
BOMwith the versions of all Kora artifacts (required, no default).- All Kora annotation processors for the main sources (required, no default).
- The same processors for the test sources — needed only if the test sources declare Kora annotations of their own, for example their own
@KoraApp(optional).
build.gradle.kts:
dependencies {
implementation(platform("io.koraframework:kora-bom:2.0.0.RC1")) //(1)!
ksp("io.koraframework:symbol-processors:2.0.0.RC1") //(2)!
kspTest("io.koraframework:symbol-processors:2.0.0.RC1") //(3)!
}
BOMwith the versions of all Kora artifacts (required, no default).- All Kora
KSPprocessors for the main sources (required, no default). - The same processors for the test sources — needed only if the test sources declare their own
@KoraApp, for example a separateTestApplication(optional).
After that, module dependencies can be declared without a version, for example:
All Kora artifacts live in the io.koraframework group.
The exception is the experimental modules — the declarative S3 client s3-client-kora and the Camunda integrations camunda-engine-bpmn, camunda-rest-undertow, camunda-zeebe-worker, together with their processors — they are published in the io.koraframework.experimental group.
Run¶
A Kora application is an interface annotated with @KoraApp that inherits the modules the application needs.
The processor generates a class next to it, named after the interface with a Graph suffix, and its static graph() method returns the description of the dependency graph:
@KoraApp
public interface Application extends HoconConfigModule, JsonModule, LogbackModule, UndertowPublicHttpServerModule { //(1)!
static void main(String[] args) {
KoraApplication.run(ApplicationGraph::graph); //(2)!
}
}
- The
@KoraAppannotation lives inio.koraframework.common.annotation, the modules are contributed by the connected Kora artifacts. ApplicationGraphis generated by the processor from theApplicationinterface name and is placed in the same package.KoraApplication.runaccepts that description (ApplicationGraphDraw), initializes the graph, registers a shutdown hook, and blocks the thread until the application shuts down.
@KoraApp
interface Application : HoconConfigModule, JsonModule, LogbackModule, UndertowPublicHttpServerModule //(1)!
fun main() {
KoraApplication.run(ApplicationGraph::graph) //(2)!
}
- The
@KoraAppannotation lives inio.koraframework.common.annotation, the modules are contributed by the connected Kora artifacts. ApplicationGraphis generated by the processor from theApplicationinterface name and is placed in the same package.KoraApplication.runaccepts that description (ApplicationGraphDraw), initializes the graph, registers a shutdown hook, and blocks the thread until the application shuts down.
The application plugin is usually used for local startup and building an executable archive.
[!TIP] It's recommended to always use fixed values
applicationName = "application"andarchiveFileName = "application.tar"— this simplifies working with the archive inDockerfileand CI/CD scripts, as the filename doesn't depend on the project version.
Connect the plugin in build.gradle:
- The
applicationplugin provides tasks for running and building an executable archive (not connected by default, optional)
System properties and environment variables for local startup can be set in the run task:
- JVM arguments for running the application (not set by default, optional)
- Environment variables available in the application (not set by default, optional)
Run:
Archive build configuration:
application {
applicationName = "application" //(1)!
mainClass = "io.koraframework.example.Application" //(2)!
applicationDefaultJvmArgs = ["-Dfile.encoding=UTF-8"] //(3)!
}
distTar {
archiveFileName = "application.tar" //(4)!
}
- Application name, used for naming scripts (default: project name). It's recommended to fix the value
"application"to simplify work inDockerfileand CI/CD. - Fully qualified name of the class with the
mainmethod for running (not set by default, required). - Default JVM arguments for running (not set by default, optional). If the application uses
StructuredTaskScope,--enable-previewmust be added here as well. - Archive filename (default:
<applicationName>-<version>.tar). It's recommended to fix the value"application.tar"to simplify work inDockerfileand CI/CD. More details in theTartask documentation.
Build the archive:
A configured application example is available in the Java application template.
Connect the plugin in build.gradle.kts:
plugins {
id("application") //(1)!
kotlin("jvm") version "2.4.10"
id("com.google.devtools.ksp") version "2.3.11"
}
- The
applicationplugin provides tasks for running and building an executable archive (not connected by default, optional)
System properties and environment variables for local startup can be set in JavaExec tasks:
tasks.withType<JavaExec> {
jvmArgs(
"-Xmx256m", //(1)!
)
environment(
"SOME_ENV" to "someValue", //(2)!
)
}
- JVM arguments for running the application (not set by default, optional)
- Environment variables available in the application (not set by default, optional)
Run:
Archive build configuration:
application {
applicationName = "application" //(1)!
mainClass.set("io.koraframework.example.ApplicationKt") //(2)!
applicationDefaultJvmArgs = listOf("-Dfile.encoding=UTF-8") //(3)!
}
tasks.distTar {
archiveFileName.set("application.tar") //(4)!
}
- Application name, used for naming scripts (default: project name). It's recommended to fix the value
"application"to simplify work inDockerfileand CI/CD. - Fully qualified name of the class with the
mainmethod for running (not set by default, required); for Kotlin this is a class with theKtsuffix. - Default JVM arguments for running (not set by default, optional). If the application uses
StructuredTaskScope,--enable-previewmust be added here as well. - Archive filename (default:
<applicationName>-<version>.tar). It's recommended to fix the value"application.tar"to simplify work inDockerfileand CI/CD. More details in theTartask documentation.
Build the archive:
A configured application example is available in the Kotlin application template.
Terminology¶
This section describes the basic terms used throughout the Kora documentation:
- Factory - a method that creates and returns an instance of a component or dependency.
- Module - a connected dependency or an interface with factory methods that add new components to the application.
- Component - an object in the Kora dependency graph. Usually a single instance of a class that implements a part of the application logic.
- Aspect - logic that extends the behavior of a method before, after, or around its execution based on an annotation.
- Dependency graph - the set of application components and the links between them, built by Kora at compile time.
First Guide¶
After this general overview, continue with the Creating Your First Kora Application guide.
It shows the basic application structure on a small HTTP service that can be built and run.