Java or Kotlin for Quarkus?
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus officially supports Kotlin as a first-class language — code.quarkus.io’s project generator offers a Java/Kotlin language picker next to the build-tool choice, and the framework ships a dedicated extension whose whole job is smoothing over the language differences that would otherwise bite a Kotlin Quarkus codebase under build-time augmentation. This page covers that extension and its compiler plugins, a pros/cons comparison re-grounded in Quarkus specifics rather than restated Spring content, a side-by-side minimal REST resource, and guidance on when a Quarkus-specific driver tips the choice one way or the other. It mirrors the structure of the equivalent Java-or-Kotlin page for Spring Boot — read that page for the SpringBoot-specific angle on the same underlying language choice.
First-Class Kotlin Support
Adding Kotlin support to a Quarkus project starts with the kotlin extension:
./mvnw quarkus:add-extension -Dextensions=kotlin
which wires in the Kotlin standard library and the Kotlin Maven/Gradle build-plugin support. Alongside it, any Kotlin Quarkus project needing CDI proxies or Panache active-record entities should also configure two Kotlin compiler plugins — they are not pulled in by the extension itself, so skipping this step is a common source of silent proxy/constructor failures (a project generated from https://code.quarkus.io/ already has both set up):
-
The all-open compiler plugin opens (makes non-
final) any class annotated with a configured annotation. Quarkus’s build-time CDI implementation, ArC (see CDI and the Programming Model), needs to generate a client proxy subclass for normal-scoped beans (@ApplicationScoped,@RequestScoped) and for any bean wrapped by an interceptor or decorator — the same requirement Spring’s own CGLIB proxying imposes on@Service/@Configurationbeans, which is exactly whykotlin-spring’s all-open preset exists on that side. Kotlin classes are `finalby default, so without this plugin every proxied class would need a manualopenkeyword, easy to forget and easy to get wrong silently. -
The no-arg compiler plugin adds a synthetic no-argument constructor to any class annotated with a configured annotation. Panache active-record entities (see Panache) are ordinary Hibernate ORM
@Entityclasses underneath, and Hibernate needs a no-arg constructor to instantiate them when reading from the database — the same constraintkotlin-jpasolves for Spring, applied here to Panache’s@Entity-annotated active-record and repository-style entities alike. Unlike CDI’s client proxies above, Quarkus’s build-time bytecode enhancement of entities (see Hibernate ORM in Quarkus) handles field-level lazy-loading access separately, so entity classes don’t also need the all-open plugin for that purpose.
Maven
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>all-open</plugin>
<plugin>no-arg</plugin>
</compilerPlugins>
<pluginOptions>
<option>all-open:annotation=jakarta.ws.rs.Path</option>
<option>all-open:annotation=jakarta.enterprise.context.ApplicationScoped</option>
<option>all-open:annotation=io.quarkus.test.junit.QuarkusTest</option>
<option>no-arg:annotation=jakarta.persistence.Entity</option>
</pluginOptions>
</configuration>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals><goal>compile</goal></goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals><goal>test-compile</goal></goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Gradle
// build.gradle.kts
plugins {
id("io.quarkus")
kotlin("jvm") version "2.4.0"
kotlin("plugin.allopen") version "2.4.0"
kotlin("plugin.noarg") version "2.4.0"
}
allOpen {
annotation("jakarta.ws.rs.Path")
annotation("jakarta.enterprise.context.ApplicationScoped")
annotation("io.quarkus.test.junit.QuarkusTest")
}
noArg {
annotation("jakarta.persistence.Entity")
}
Neither plugin is Quarkus-specific machinery — they are the same general-purpose Kotlin compiler plugins
documented on kotlinlang.org, configured here against the Jakarta/Quarkus annotations that need opening or a
no-arg constructor. A constructor-injected class with no interceptor bindings and no normal scope requiring a
proxy (a @Dependent bean, for instance) needs neither, the same nuance the side-by-side example below calls
out concretely.
Pros and Cons
| Dimension | Java | Kotlin |
|---|---|---|
Null safety |
no compile-time distinction — |
nullability is part of the type ( |
Concurrency: virtual threads vs. coroutines |
|
Kotlin coroutines give similar scalability with |
Boilerplate |
constructors, |
|
Build-time augmentation / all-open |
no extra step — non- |
requires the all-open and no-arg compiler plugins above; Quarkus’s closed-world build-time-augmentation model
means this is resolved once, at compile time, rather than Spring’s runtime CGLIB proxying — the same underlying
constraint (a non- |
Native image / GraalVM |
no Quarkus-specific extra step beyond the general native-image guidance on Native Executables with GraalVM/Mandrel. |
Kotlin native-image support is mature and well-trodden in the Quarkus ecosystem, not a second-class path — but
some reflection-heavy parts of the Kotlin standard library (occasional |
Java interop / ecosystem |
native — every Quarkus extension and Jakarta EE API targets Java directly. |
fully interoperable, but Quarkiverse’s community extension coverage for Kotlin, while good, is narrower than core Quarkus’s own first-party extension set — worth checking before committing to a Kotlin-only pattern for a less mainstream integration. |
Compile times / tooling (K2) |
|
the modern K2 compiler closed most of the historical Kotlin-vs-Java compile-time gap, but a large multi-module Kotlin build can still compile somewhat slower than the equivalent Java one. |
Hiring / ramp-up |
larger overall talent pool; most backend engineers already know Java. |
smaller pool specifically for backend Kotlin, though most Java engineers ramp up quickly given deliberate Java interoperability and a similar OOP model. |
Android code sharing |
none — Android’s officially preferred language is Kotlin, so a Java backend shares no source with an Android client team. |
a team already writing Kotlin for an Android app can share DTOs/validation logic/business rules with a Kotlin Quarkus backend, and staff more fluidly across both. |
Side by Side: a Minimal @Path Resource
package com.example.book;
import java.util.Optional;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.core.Response;
@Path("/api/books")
@ApplicationScoped
public class BookResource {
private final BookRepository repository;
@Inject
public BookResource(BookRepository repository) {
this.repository = repository;
}
@GET
@Path("/{id}")
public Response getBook(@PathParam("id") Long id) {
return Optional.ofNullable(repository.findById(id))
.map(book -> Response.ok(book).build())
.orElseGet(() -> Response.status(Response.Status.NOT_FOUND).build());
}
}
package com.example.book
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.core.Response
@Path("/api/books")
@ApplicationScoped
class BookResource(private val repository: BookRepository) { // constructor injection, no boilerplate
@GET
@Path("/{id}")
fun getBook(@PathParam("id") id: Long): Response =
repository.findById(id)
?.let { Response.ok(it).build() }
?: Response.status(Response.Status.NOT_FOUND).build()
}
The Kotlin version’s null-handling reads directly from
Null Safety's ?.let { } / ?: idiom, in place of Java’s
Optional.map/.orElseGet. Because BookResource here is @ApplicationScoped — a normal scope ArC backs with
a client proxy — it does need the all-open plugin from the previous section to open the class for that proxy;
a resource left without an explicit scope annotation would instead default to @Singleton (the scope Quarkus
REST applies to unannotated @Path classes) and would not need it, since @Singleton is a pseudo-scope that ArC
never proxies, and Kotlin’s final-by-default classes cause no problem for a plain, non-proxied class.
When to Choose Which
Neither language is a strictly better default — the right choice tracks the team and the surrounding context
more than any single row in the table above. Kotlin earns its keep fastest when a team already knows it (most
concretely, an Android team extending into backend work and sharing models with their existing app), when null
safety and reduced boilerplate would measurably cut a class of bugs the team already fights, or when the team is
already comfortable with a linear, sequential coroutine style and would rather write suspend fun than compose
Uni/Multi operator chains. The reverse also holds: a team already fluent in Mutiny’s reactive operators, or
one whose codebase leans heavily on reactive Quarkus extensions that return Uni/Multi throughout, may find
staying within that idiom simpler than introducing coroutines and the kotlinx-coroutines interop layer for a
single service. Java remains the steadier default for a large, existing Java codebase, a team without prior
Kotlin exposure and no immediate driver to gain one, or a project that leans on a Quarkiverse extension with
Kotlin-unfriendly or reflection-heavy usage patterns not yet proven under native image. This is guidance, not a
mandate — both compile to the same bytecode, run under the same build-time augmentation, and are first-class
citizens in Quarkus either way.