Java or Kotlin for Spring Boot?

This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases.

This section’s bibliography lists the reference material consulted while preparing these pages.

Spring Boot officially supports Kotlin as a first-class language, not merely as "a JVM language that happens to work" — the Spring Initializr’s own project wizard offers a Java/Kotlin language picker right alongside the build-tool choice, and the framework ships dedicated Gradle plugins specifically to smooth over the language differences that would otherwise bite a Kotlin Spring Boot codebase.

First-Class Kotlin Support

Two Kotlin Gradle plugins exist specifically because of how Spring works internally, and any Kotlin Spring Boot project should apply both:

// build.gradle.kts
plugins {
    id("org.springframework.boot") version "4.1.0"
    kotlin("jvm") version "2.4.0"
    kotlin("plugin.spring") version "2.4.0"    // "kotlin-spring": auto-opens @Configuration/@Service/etc.
    kotlin("plugin.jpa") version "2.4.0"        // "kotlin-jpa": auto-opens @Entity, adds a no-arg constructor
}
  • kotlin("plugin.spring") solves the problem described in Classes and Objects: Kotlin classes are final by default, but Spring needs to CGLIB-subclass a @Configuration/@Service/@Component/@Controller class to install its proxies (for AOP, @Transactional, and similar). The plugin automatically makes every class annotated with a Spring stereotype annotation open, with no manual open class needed anywhere.

  • kotlin("plugin.jpa") solves the equivalent problem for JPA: Hibernate needs a no-argument constructor and non-final classes/properties to generate lazy-loading proxies for @Entity classes. The plugin adds both automatically for any class annotated @Entity, @MappedSuperclass, or @Embeddable.

Pros and Cons

Dimension Java Kotlin

Null safety

no compile-time distinction — Optional helps at API boundaries but ordinary fields/parameters can always be null; NPEs remain a runtime risk.

nullability is part of the type (String vs. String?, see Null Safety) — the compiler forces null handling at every call site, eliminating a large class of NPEs before they ship.

Boilerplate

constructors, equals/hashCode/toString, and getters/setters are hand-written or generated by an annotation processor (Lombok) — see Lombok and MapStruct.

data class (Data Classes and Destructuring) generates all of that from one line, with no extra dependency or annotation processor.

Concurrency model

virtual threads (Project Loom, Virtual Threads) let blocking-style code scale without the reactive-programming tax — Spring Boot 4’s default choice for new blocking-style services.

coroutines (Coroutines Basics) give similar scalability with suspend functions; Spring MVC and WebFlux controllers can both declare suspend fun handlers directly.

Framework proxying / all-open

no extra step — non-final classes are the Java default, so CGLIB proxying "just works."

requires the kotlin-spring plugin (above) to open Spring-managed classes automatically; without it, every @Service/@Configuration class needs a manual open — easy to forget.

JPA/Hibernate interop

no extra step — a no-arg constructor and non-final fields are the norm already.

requires the kotlin-jpa plugin (above) for the same reason; data class entities also need care around equals/hashCode on mutable, ID-generated entities (a well-known Hibernate pitfall independent of language).

Java interop / ecosystem

native — every Spring/Jakarta EE library targets Java directly.

fully interoperable (Kotlin and the JVM), but occasional friction at the edges: platform types from unannotated Java APIs, or a Java library’s fluent builder reading less naturally from Kotlin than apply { } (Extension Functions and Scope Functions) would.

Compile times / tooling (K2)

javac incremental compilation is fast and mature.

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 the deliberate Java interoperability and similar OOP model.

Android code sharing

none — Android’s officially preferred language is Kotlin (Kotlin for Android), 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 Spring Boot backend, and staff more fluidly across both.

Side by Side: a Minimal @RestController

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        return userService.findById(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {   // constructor injection, no boilerplate

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<UserDto> =
        userService.findById(id)
            ?.let { ResponseEntity.ok(it) }
            ?: ResponseEntity.notFound().build()
}

The Kotlin version needs no kotlin-spring open annotation here (Spring proxies the class via the interfaces it implements when there are any, and constructor-injected, non-proxied beans in general do not require open at all — the plugin matters most for classes proxied by subclassing, such as many @Configuration classes and @Transactional-annotated services). Its null-handling reads directly from Null Safety's ?.let { } / ?: idiom in place of Java’s Optional.map/.orElseGet.

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, sharing models with their existing app), when null safety and reduced boilerplate would measurably cut down a class of bugs the team already fights, or when the project is new enough that ramp-up cost is a one-time thing rather than a migration. 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 heavily on Java-only tooling/annotation processors that have no mature Kotlin story. This is guidance, not a mandate — both compile to the same bytecode, run on the same JVM, and are first-class citizens in Spring Boot either way.

References