The Friction Zone: Why mixing Kotlin, Java, Hibernate and Lombok is a technical debt nightmare

Updated: 2026-01-16

What could go wrong mixing Java, Kotlin and Hibernate in the same project? Many things. If you add Gradle and Lombok in the mix ... welcome to the disaster.

Transitioning a legacy Java monolith to Kotlin sounds like a modern developer’s dream, but when that project relies on Hibernate and Lombok, the "100% interoperability" promise quickly unravels. This post explores the deep-rooted architectural conflicts that occur when Kotlin’s opinionated, null-safe design meets the "anything-goes" reflection-heavy world of JPA.

Why the mix?

The good reason behind the mix of technologies is the goal to use the cool new features that Kotlin brings to the Java world staying in the same ecosystem.
Kotlin is a perfect valid solution for modern development, the complexity is generated when you try to mix it with the 'standard' ecosystem of the Java world.

Kotlin and Hibernate ... the marriage to avoid

Many business applications use Hibernate as a persistence layer. Hibernate is a great framework (if used correctly). The issue is that Hibernate has been built with the 'old' Java OO design that allowed everything. Kotlin is an opinionated language built with a completely different design.

The "Final" Boss: Open Classes and Proxies

By default, Kotlin classes are final. In the Kotlin world, this is a feature—it encourages composition over inheritance. In the Hibernate world, this is a potential catastrophical problem.

Hibernate loves to create proxies. When you call entityManager.getReference(), Hibernate creates a subclass of your entity on the fly to handle lazy loading. If your class is final, Hibernate can’t subclass it. This doesn't seems dangerous until when you understand that without proxy Hibernate will fetch everything eagerly with the possibility that a simple select will retrieve all your database.

The Fix: You could manually add the open keyword to every class and property or use the Kotlin all-open plugin. It compiler-magically opens up classes annotated with @Entity so Hibernate can do its proxy magic ... on the other side you already violated one of the Kotlin design principle.

This open class requirement is not only for Hibernate, it's a requirement for Spring AOP too. Spring loves to proxy classes and these cannot be final. Spring uses CGLIB to subclass the annotated classes and this doesn't work with final.

Data Classes ... no ... it's not so easy

Every new Kotlin dev sees data class and thinks, "Perfect! My JPA entities are just data holders!" :)

Stop, it's not so easy. Data classes automatically generate equals(), hashCode(), and toString(). While this sounds great, they use every property defined in the primary constructor.

  • Circular Dependencies: If Entity A has a Set<Entity B> and Entity B has a reference back to A, calling toString() or hashCode() triggers a recursive loop that ends in a StackOverflowError.
  • The Identity Crisis: Hibernate entities need an identity that is consistent across states (Transient, Persistent, Detached). If your hashCode is based on a mutable field or a generated ID that is null before saving, your entity will "disappear" from a HashSet once it’s persisted.
  • Where is my constructor? : Hibernate requires an empty constructor (no-args constructor). Kotlin data class doesn't like it and doesn't allow it ... it makes sense a data class is required to contains data, not empty values.
    How to fix this problem? Kotlin offers a plugin to handle JPA style classes_ the no-arg compiler plugin. If you want to avoid another plugin you can simply use a simple class losing the nice features of the data class. Whatever you choose you will break again the basic design of Kotlin.

nullability ... and Hibernate

Kotlin’s primary selling point is null safety. Java’s old selling point is... well, it lets you do whatever you want.

Hibernate often populates fields using reflection, bypassing constructors. If you declare a field as non-nullable (String), but the database column has a NULL, Hibernate will happily shove that null into your non-nullable Kotlin property.

When you eventually try to access that property in Kotlin, the JVM will throw an IllegalStateException because the "non-null" variable is actually null.

The Fix: 1. Use nullable types (String?) for fields that are nullable in the DB. 2. Use the Kotlin No-Arg plugin. Hibernate requires a no-argument constructor to instantiate entities. Without this plugin, you’re stuck writing awkward "default" values for every field.

Collection Confusion: List vs. MutableList

Kotlin makes a strong distinction between List (read-only) and MutableList. Hibernate, however, wants to inject its own persistent collection wrappers. It uses his own PersistentBag to fill the lists only when necessary (lazy loaded).

If you define your collection as:
val members: List<User> = listOf()

Hibernate will struggle to swap that out with its own proxy collection. To keep the peace, you’re often forced to use MutableList and initialize it with a mutable helper, which feels a bit like wearing socks with sandals—it works, but you the Italians won't appreciate.

Gradle ... a major pain

Gradle is probably the most hated feature related to Kotlin ... useless to say that it doesn't play very well in the mixed environment.

Gradle & JDK Compatibility

As of 2026, the rapid release cycle of the JDK has created a "version lag" between Gradle and the Kotlin compiler.

  • Version mismatch: Gradle 9.x is required to fully support JDK 25 features, but the Kotlin Gradle Plugin (KGP) often lags behind by several months. If you attempt to use a cutting-edge JDK as your Gradle daemon, you will frequently encounter UnsupportedClassVersionError or internal compiler crashes during the stub generation phase. You will have to use an old JDK.

Lombok ... is saving Java and a pain with Kotlin

The Compilation Order Paradox

The fundamental issue is that Kotlin and Lombok are both trying to perform "magic" during the compilation phase, but they do it in different ways.

  • Kotlin compiles first. It looks at your Java source files to understand the types and methods it needs to call.
  • Lombok isn't a compiler; it’s an Annotation Processor that manipulates the Java Abstract Syntax Tree (AST) during the Java compilation phase.

As a result, if you have something like this:

@Data 
public class User { 
  private String email; 
} 

Kotlin sees a private field email and zero methods. Because Lombok hasn't run yet, the getEmail() method doesn't exist in the eyes of Kotlin. You’ll get Unresolved reference error, even though IntelliJ (thanks to the Lombok plugin) tells you everything is fine. You can add manually add getters and setters ... Kotlin will be happy for this, IntelliJ will gently complains that you should remove them.

Another options is ... another JetBrains plugin, it's still experimental (a.k.a. don't use it): https://kotlinlang.org/docs/lombok.html

Optional ... or not

In Java we learnt how to use and mis-use Optional. Kotlin has this feature (?) backed in its original design. Our problem, in a mixed language project, is to use it correctly.

  • Java to Kotlin
    If a Java method returns Optional<User>, Kotlin sees it as the type Optional<User>!. Kotlin does not automatically convert this to User?. You have to handle the Optional API manually:
val user: User? = userRepository.findByName("Marco").orElse(null) 

If return from Java a String (could be null in Java), Kotlin will expect a String! with the risk of a NullPointerException. You should annotate your Java method returns with @Nullable or @NotNull to avoid confusion.

  • Kotlin to Java
    If a Kotlin function returns User?, Java sees it as a regular User that might be null. To make it friendly for your Java colleagues who expect an Optional, you have to wrap it yourself:
// Kotlin function designed for Java callers 
fun findUser(id: String): Optional<User> { 
    return Optional.ofNullable(internalSearch(id)) 
} 

Summary and personal opinion

Can you use Java, Kotlin, Hibernate and Lombok in the same project? YES

You can use it as a temporary solution for a migration between languages and frameworks. We did this and it was not easy as we thought "everything runs in the same JRE".

Kotlin and KMP are great technologies for many use cases, but I'd recommend to don't mix it with Hibernate. The original design and philosophy is totally different. Spring JDBC or some Kotlin specific ORMs (Exposed, Ktorm) are easier solutions.

StackWork well together?
Spring + KotlinYES
Kotlin + HibernateNO, avoid!
Java + KotlinMore or less
Java / Lombok + KotlinMore or less

WebApp built by Marco using Java 24 - Hosted in Switzerland