Kotlin and Spring Boot in Practice: A Field Report
Kotlin and Spring Boot in Practice: A Field Report
At the end of 2018, we deliberately chose Kotlin over Java as the language for a Spring Boot backend in a new client project for the first time. Now, over two years later, Kotlin is our default language for new Spring projects. Time for an honest assessment: what has proven itself, where did we stumble, and would we take the same path again?
The First Months: Java in Kotlin Syntax
If you're honest, you have to admit: for the first one to two years, you write Kotlin that looks a lot like Java. You declare variables with val instead of final, use data classes instead of POJOs, and enjoy the shorter syntax. But you still think in Java patterns. Extension functions, scope functions like let, apply, or also, sealed classes -- all of that only comes with time, once you've truly internalized the language.
That's not a disadvantage. On the contrary: Kotlin rewards a gradual learning process. The code becomes more idiomatic month by month without losing productivity. New team members with a Java background are productive within one to two weeks. The onboarding is significantly smoother than switching to a functional language like Scala.
What Works Excellently
Data Classes: The Most Obvious Win
In every Spring Boot project, there are dozens of DTOs, request objects, and response classes. In Java, that means getters, setters, constructors, equals(), hashCode(), toString() -- either written by hand or generated via Lombok. In Kotlin, it's a single line.
The difference in practice:
Java (with Lombok):
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CreateOrderRequest {
@NotBlank private String customerId;
@NotNull private List<OrderItem> items;
private String comment;
private LocalDate deliveryDate;
}
Kotlin:
data class CreateOrderRequest(
@field:NotBlank val customerId: String,
@field:NotNull val items: List<OrderItem>,
val comment: String? = null,
val deliveryDate: LocalDate? = null
)
At first glance, you only save a few lines. The real advantage runs deeper: Kotlin data classes are immutable by default. The copy() function makes defensive copies trivial. And the nullable type system shows directly in the class definition which fields are optional -- no more @Nullable annotation chaos. In a project with over 80 DTOs, this adds up to a significant reduction in boilerplate and markedly better readability.
Null Safety in Production
Kotlin's null safety has noticeably reduced the number of NullPointerException errors in production across our projects. That sounds like it should go without saying, but it's remarkable in practice. The compiler forces you to deal with every potential null value -- at compile time, not at runtime.
This is especially valuable at the boundaries of the system: where data comes from external APIs, databases, or user input. In Java, you trust that the @NotNull annotation delivers on its promise. In Kotlin, the type is either nullable or not -- the compiler gives you no choice.
Extension Functions in the Spring Context
Extension functions are one of those features you initially underestimate in Kotlin and then can't live without. In our projects, we use them primarily for two things: first, to make frequently recurring conversions readable, and second, to create test utilities.
fun Customer.toResponse() = CustomerResponse(
id = this.id,
name = "${this.firstName} ${this.lastName}",
email = this.email,
memberSince = this.createdAt.toLocalDate()
)
// Then simply in the controller:
@GetMapping("/{id}")
fun getCustomer(@PathVariable id: Long): CustomerResponse =
customerService.findById(id).toResponse()
It's not a revolutionary feature. But it makes the code consistently more readable because the conversion logic is defined where it belongs -- close to the data type, without bloating the entity class itself.
The Pitfalls
JPA and Kotlin: It Works, but Needs Some Help
Writing JPA entities in Kotlin is possible but requires two compiler plugins: kotlin-jpa (generates the no-arg constructor that JPA needs) and kotlin-allopen (makes entity classes open, since Hibernate needs proxies). Without these plugins, you get cryptic runtime errors.
The configuration in build.gradle.kts:
plugins {
kotlin("plugin.spring")
kotlin("plugin.jpa")
kotlin("plugin.allopen")
}
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.Embeddable")
annotation("javax.persistence.MappedSuperclass")
}
Once configured, it works reliably. But the initial debugging cost us several hours. Another point: data classes and JPA entities don't fit perfectly together. JPA expects mutable entities with a stable identifier for equals() and hashCode(). We've moved to writing JPA entities as regular classes and using data classes only for DTOs -- a separation that has proven effective.
Database Access: The Agony of Choice
Alongside JPA with Hibernate, we've also evaluated alternatives in some projects. Spring JdbcTemplate works flawlessly with Kotlin, and the newer JdbcClient in Spring 6 makes it even more pleasant. Exposed, JetBrains' Kotlin-native ORM framework, offers an elegant DSL but has a smaller ecosystem. For most of our projects, we stick with JPA because the adoption and Spring Data support are unbeatable. But anyone starting a new project without JPA legacy should at least take a look at Exposed.
Coroutines: Potential, but Not Yet Part of Our Daily Workflow
Kotlin Coroutines promise elegant asynchronous programming. Impressive in theory, but in our practice they still play a minor role. Most of our services use Spring MVC with blocking I/O -- and that works excellently for our use cases. Switching to coroutines would mean also migrating the entire persistence layer to non-blocking drivers. The effort isn't justified by the benefit for us right now.
Where we do use coroutines selectively is for parallelizing independent service calls -- for example, when an aggregation endpoint merges data from three internal APIs. Here, async/await genuinely makes the code more readable than CompletableFuture. But we haven't taken that step across the board yet.
Kotlin DSLs in Practice
An advantage you only learn to appreciate over time: many libraries now offer Kotlin DSLs. Gradle Kotlin DSL instead of Groovy, Rest Assured with Kotlin extensions, Testcontainers with Kotlin support. These DSLs feel more natural and provide real type safety -- no more guessing which methods are available.
Conclusion: We Would Do It Again
After more than two years of Kotlin in Spring Boot projects, our verdict is clearly positive. The language reduces boilerplate, prevents entire classes of bugs through null safety, and makes code more expressive. The interoperability with Java is excellent -- we continue to use the entire Spring ecosystem without restrictions.
The pitfalls are real but manageable. The JPA plugin configuration is a one-time affair. The learning curve for Java developers is moderate. And the few areas where Kotlin rubs against the Java ecosystem are well documented.
What surprised us the most: the biggest gain doesn't lie in any single feature but in the sum of many small improvements. Fewer lines of code, fewer sources of errors, more readable tests, more expressive APIs. In each individual case, the difference is modest. Across an entire project, it changes the way you work.
For new Spring Boot projects, Kotlin is now our default choice. Not because Java is bad -- Java 16 has caught up with records and pattern matching. But because Kotlin is already the language today that Java wants to be in a few years.
Written by
Patrick HütterFounder & Software Architect
Software architect, engineer and entrepreneur. Patrick has been building products and platforms for over a decade — from enterprise backends and cloud-native infrastructure to AI-powered applications. As founder of encircle360, he combines deep technical expertise with entrepreneurial vision, driving open source projects that create real impact.
You might also like
From Manifest to Production: How ADL, A2A and the Inference Gateway Are Revolutionizing Agent Infrastructure
Jul 5, 2026 · 12 min read
Agent Orchestration with Java: Bringing LLM Agents to Production on the JVM
Jul 4, 2026 · 6 min read
Spring AI: How Java Developers Can Finally Integrate AI Features the Right Way
Mar 25, 2026 · 5 min read