September 15, 2025
Why I Chose Kotlin Over Java for New Spring Boot Projects
After years of writing Java Spring Boot backends, I switched to Kotlin. Here's what I've learned, the gotchas no one tells you, and whether it's worth the migration.

When I joined my current team, the codebase was Java 17 Spring Boot. Standard setup. Annotations everywhere, verbose getters/setters, the works. Six months in, when we started greenfielding a new microservice, I pushed to use Kotlin. The team was skeptical.
Here's what I told them, and what I've learned since.
The Honest Sales Pitch
Kotlin is not a revolution. It's Java, refined. If you know Spring Boot in Java, you can be productive in Kotlin within a week. The interoperability is genuinely excellent: you can mix Kotlin and Java files in the same project, call each other's code, share the same build toolchain.
What Kotlin does give you is less ceremony for the same intent:
// Java
public class CreateOrderRequest {
private final String customerId;
private final List<String> productIds;
public CreateOrderRequest(String customerId, List<String> productIds) {
this.customerId = customerId;
this.productIds = productIds;
}
public String getCustomerId() { return customerId; }
public List<String> getProductIds() { return productIds; }
}// Kotlin
data class CreateOrderRequest(
val customerId: String,
val productIds: List<String>
)Same semantics. One line vs. twenty. And the data class gives you equals, hashCode, toString, and copy for free.
When your codebase has fifty DTOs and request/response classes, this adds up.
Null Safety is the Real Win
Java's @NotNull / @Nullable annotations are documentation. They don't prevent NPEs. The compiler doesn't care.
Kotlin's null safety is enforced at the type level:
fun processUser(user: User?) {
// user?.name is null-safe access
// user!!.name throws NPE if null — compiler forces you to acknowledge the risk
val name = user?.name ?: "Anonymous"
println("Hello, $name")
}After six months of production Kotlin code, I've had zero NPEs in the Kotlin services. Not because I'm particularly careful, but because the compiler makes it structurally hard to be careless.
Extension Functions Change Everything
Spring Boot relies heavily on annotations. That's fine. But sometimes you want utility functions without polluting your classes or creating SomeUtil singletons.
Extension functions let you add behavior to existing types:
fun String.toSlug(): String =
this.lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
// Usage
val slug = "Why I Chose Kotlin".toSlug() // "why-i-chose-kotlin"I use this constantly for transformations tied to the domain.
The Gotchas
1. Spring AOP and open classes
By default, Kotlin classes are final. Spring's AOP proxying (which powers @Transactional, @Cacheable, etc.) requires classes to be open. The fix is the kotlin-spring compiler plugin, which automatically opens classes annotated by Spring. Add it to your build, but know why you need it.
// build.gradle.kts
plugins {
kotlin("plugin.spring") version "1.9.25"
}2. JPA and the kotlin-jpa plugin
JPA requires no argument constructors. Kotlin data classes don't have them by default. Same solution: kotlin-jpa plugin.
3. Jackson serialization quirks
The jackson-module-kotlin dependency is not negotiable. Without it, you'll get mysterious serialization errors on data classes. Add it, never think about it again.
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")4. Coroutines are a different mental model
If you want to use Kotlin coroutines with Spring WebFlux (the reactive stack), there's a learning curve. Coroutine code that looks sequential hides asynchronous behavior. Make sure your team understands what suspend actually means before committing to the pattern.
Is It Worth It for a Migration?
For a migration of an existing Java codebase: probably not, unless you're doing a major rewrite anyway. The interop works, but you'll accumulate inconsistency.
For a new service in a shop that mostly uses Java: yes. The incremental productivity gain is real. Onboarding a Kotlin service alongside existing Java services is straightforward.
For a greenfield project with no legacy: yes, and pick the Spring Boot Kotlin starter from initializr.
My Setup
Here's what I use in production Kotlin Spring Boot services:
- Kotlin 1.9.x + Spring Boot 3.x
jackson-module-kotlinfor serializationkotlin-springandkotlin-jpacompiler plugins- Exposed or Spring Data JPA (prefer Exposed for greenfield, since the DSL is more idiomatic)
- Coroutines only where the service is genuinely I/O bound; otherwise synchronous code reads better
The philosophy: use Kotlin features where they reduce noise. Don't force coroutines where blocking is fine. Don't contort your domain model to be "more functional" if OOP serves it better.
Use the language. Don't perform it.
Discussion
Discussion is unavailable; Giscus is not configured for this build.