Skip to content
Kona

Kotlin and Java

Kona is written in Kotlin, but Java applications are a first-class target. Every public API is shaped so that a Java call site reads naturally, and the Kona repository keeps a Java sample (samples/hello-java) next to the Kotlin one (samples/hello-kotlin) as part of its regression tests. When a page in these docs shows both languages, the behavior is identical; only the syntax differs.

This page explains the conventions behind that, so you can predict how any Kona API looks from your language without hunting for an example.

Kotlin compiles to ordinary JVM classes. A few language features translate in predictable ways:

Kotlin declaration What Kotlin code writes What Java code writes
Property val container app.container app.getContainer()
Mutable property var status response.status = 201 response.setStatus(201)
object Kona with @JvmStatic members Kona.create(args) Kona.create(args)
Nested class Application.Builder Application.Builder Application.Builder
fun interface Handler trailing lambda { request -> ... } lambda request -> ... or method reference
Function type (Container) -> T { c -> Greeter() } c -> new Greeter()
Return type T? (nullable) compiler forces a null check may return null; check it yourself

Kona marks its entry points (Kona, Env, Application.builder, Response.text/json/html, Request.get/post, Config.toBoolean) with @JvmStatic, so Java calls them as plain static methods without a .INSTANCE or .Companion hop.

val app: Application = Kona.create(args)
.providers(HttpServiceProvider(), AppServiceProvider())
.configure { config -> config.set("app.name", "Demo") }
.boot()

configure accepts a java.util.function.Consumer<Config>, so it takes a Java lambda directly and a Kotlin lambda through SAM conversion.

ServiceProvider is an interface with register(Application) and boot(Application). In Kotlin, boot has an empty default body, so a Kotlin provider may leave it out. Java providers must implement both methods: the Kotlin default is not compiled as a Java default method, so leave boot empty when you have nothing to do at boot time.

class AppServiceProvider : ServiceProvider {
override fun register(app: Application) {
app.container.singleton(Clock::class.java) { SystemClock() }
}
override fun boot(app: Application) {
val router = app.resolve(Router::class.java)
router.get("/time") { Response.text(app.resolve(Clock::class.java).now().toString()) }
}
}

Types are always passed as Class objects: Clock::class.java in Kotlin, Clock.class in Java. There are no reified-generic shortcuts, so the same container call works identically in both languages.

A handler is anything that turns a Request into a Response. Handler is a single-method interface, handle(Request): Response, so any lambda or method reference with that shape fits.

router.get("/ping") { request -> Response.text("pong") }
val users = UserController()
router.get("/users") { users.index(it) }

Middleware is a function handle(request, next) that runs before a handler. Calling next passes the request further down the chain; returning without calling it ends the request early with your own response.

import org.konaframework.http.invoke // enables next(request)
val requireToken = Middleware { request, next ->
if (request.header("X-Token") == "secret") next(request)
else Response.json(mapOf("error" to "unauthorized"), 401)
}

In Kotlin, next(request) is an operator extension declared in org.konaframework.http; you must import org.konaframework.http.invoke for it to compile. next.handle(request) works in both languages without any import.

A route group applies a URL prefix, a route-name prefix, and middleware to every route declared inside it. This is the one API with genuinely different shapes per language.

router.group(prefix = "/api", name = "api.", middleware = listOf(requireToken)) {
get("/me") { Response.json(mapOf("ok" to true)) }.name("me") // GET /api/me, name "api.me"
}
// Or declare several routes with a receiver block:
router.routes {
get("/") { Response.text("home") }
post("/contact") { Response.noContent() }
}

The Kotlin group(prefix =, name =, middleware =) { } form uses named arguments and a receiver lambda, so it is hidden from Java. Java gets the fluent prefix(...).name(...).middleware(...).group(Consumer<Router>) chain and Consumer overloads instead. Both build exactly the same routes.

Kotlin parameters can have default values. Where Kona adds @JvmOverloads, Java sees one overload per optional parameter: for example Response.text(body) and Response.text(body, status), or config.get(key) and config.get(key, default). A few functions do not have Java overloads, and Java must pass every argument:

Call Kotlin Java
Environment lookup Env.get("KEY") Env.get("KEY", null), or Env.getOrDefault("KEY", "x")
Request input request.input("q") request.input("q", null)
Named route URL router.route("home") router.route("home", Map.of())

If Java reports “method cannot be applied to given types” on a Kona call, a missing default argument is the usual reason.

Calls that look things up by string key return a generic T, inferred from the variable you assign to:

val greeter: Greeter = app.container.make("greeter")
val port: Int? = app.config.get("server.port")

The cast is unchecked: if the stored value has another type, you get a ClassCastException where you use it. Prefer the typed config getters (getInt, getString, getBoolean) and class-based container lookups (make(Greeter.class)) when you can.

A Kotlin project can contain Java classes and the other way round; Kona does not care which language a provider or controller was written in. Scaffolds use standard src/main/kotlin and src/main/java roots, so you can add the other language’s sources beside the first. grind make:controller chooses .java as soon as any .java file exists under src/main/java/; pass --lang= to override.

samples/hello-kotlin and samples/hello-java in the Kona repository match the default web kona new source layout (Crema welcome + /api/health) with HTTP smoke tests. They are not full scaffolds: they have no ./grind or local ./gradlew, and you run them from the repository root (./gradlew :hello-kotlin:run / :hello-java:run). For the product path with ./grind serve, use Installation and Your first app. Other kits (api, brew, inertia) are covered by kona new regressions in the CLI test suite.