Java vs Kotlin is not just syntax: maintainability, teams, and technical debt
Java vs Kotlin from the perspective of team decisions and real maintenance, not superficial features. Experience from real projects.

In a previous article I already covered the technical comparison between Java and Kotlin: data classes, null safety, coroutines, extensions. The syntax differences are real and well documented in any tutorial. But the reality is that when you’ve been maintaining projects with both languages for a few years, the decisions that weigh the most aren’t about syntax. They’re about team, maintenance, and technical debt.
This article doesn’t rehash the feature comparison. It starts from a different point: you already know that Kotlin is more concise, that it has null safety, and that it shares the JVM with Java. The question is what happens when that meets a six-person team, a project that’s been in production for three years, and a codebase that grows every sprint.
The learning curve nobody tells you about
Kotlin’s learning curve for a Java developer is usually presented as gentle. And it’s true that writing your first data class or your first when is intuitive. But the real curve isn’t about writing basic Kotlin. It’s about writing idiomatic Kotlin, understanding Kotlin code written by others, and navigating a codebase that mixes both languages.
What actually takes time
- Extension functions. A Java dev reading
myList.filterNotNull().groupBy { it.category }needs to understand not just what it does, but where those functions are defined and whether any of them are custom to the project. - Scope functions.
let,run,apply,also,with. Each has a use case, but in practice people mix them up, overuse them, and the code becomes opaque. - Coroutines. For someone coming from Java threads or CompletableFuture, coroutines aren’t conceptually difficult, but the structured concurrency model takes time to master without creating subtle bugs.
- Type inference. Kotlin infers a lot. That’s convenient when writing, but sometimes makes reading harder when it’s not clear what type a complex expression returns.
In my experience, a senior Java developer takes about 2-3 weeks to become productive in Kotlin and about 2-3 months to write Kotlin that another Kotlin developer would review without raising an eyebrow.
The learning curve of Kotlin isn’t learning Kotlin. It’s unlearning Java. The mental patterns of Java---explicit verbosity, getters/setters, manual null checks---don’t go away in a week.
Readability: concise doesn’t always mean clearer
One of Kotlin’s most cited advantages is that you need less code to do the same thing. And that’s true. A data class in Kotlin is 3 lines; in Java (without records) it was 40. But conciseness has a tipping point where it stops helping.
When Kotlin is more readable
// Kotlin: clear, direct
val activeUsers = users
.filter { it.isActive }
.sortedByDescending { it.lastLogin }
.take(10)// Java: more verbose but equally clear
List<User> activeUsers = users.stream()
.filter(User::isActive)
.sorted(Comparator.comparing(User::getLastLogin).reversed())
.limit(10)
.collect(Collectors.toList());Here Kotlin wins cleanly. Less noise, same intent.
When Kotlin becomes opaque
// Kotlin: "elegant" but hard to follow
val result = data?.let { raw ->
raw.items
.filterNotNull()
.takeIf { it.isNotEmpty() }
?.groupBy { it.category }
?.mapValues { (_, items) ->
items.sumOf { it.price }
}
?.also { cache.store(it) }
} ?: emptyMap()This code is correct, compiles, and uses idiomatic Kotlin features. But ask a developer who didn’t write it what it does exactly. They’ll need a minute to follow the chain of let, takeIf, also, and the elvis operator. In a code review, this code generates questions.
The Java equivalent would be longer, but each step would be explicit. And explicitness, in long-term maintenance, has a value that’s underestimated.
My personal rule
If a Kotlin expression takes more than 5 seconds for someone on the team to understand what it does, it’s probably worth writing it more explicitly. Conciseness is a tool, not a goal.
Null safety in practice: beyond the feature
Kotlin’s nullable type system is, in my opinion, its best feature. It forces you to think about nulls at compile time instead of discovering them in production. But its real impact depends on how it’s used.
The good: NullPointerExceptions that never reach production
In a Java project I maintain, the top 3 production bugs are NullPointerExceptions. In the Kotlin equivalent, NPEs are practically nonexistent. Not because the code is perfect, but because the compiler forces you to handle every null case before compiling.
// The compiler won't let you ignore the null
fun getOrderTotal(order: Order?): BigDecimal {
// This doesn't compile:
// return order.total
// You have to be explicit:
return order?.total ?: BigDecimal.ZERO
}The bad: abuse of the !! operator
The !! operator (non-null assertion) exists for cases where you know a value is not null but the compiler can’t verify it. In practice, it becomes an escape valve that removes the null safety guarantee.
// This is basically going back to Java
val name = user!!.name!!.uppercase()
// If user or name are null, NPE at runtime. Same as Java.I’ve seen Kotlin codebases with hundreds of !!. At that point, null safety is decorative. If you allow !! without restriction on your team, you’re paying the cost of Kotlin without getting its greatest benefit.
The tricky case: interoperability with Java
When your Kotlin code calls a Java library, the types that come back are “platform types”---neither nullable nor non-null. The compiler doesn’t warn you. If the Java library returns null where you don’t expect it, your Kotlin code crashes just like Java.
// A Java library that returns String (platform type)
val result = javaLibrary.getResult()
// result is String! (platform type), not String or String?
// If it's null, NPE at runtime with no compiler warningThis is relevant because most Kotlin backend projects use Java libraries. Spring Framework, Jackson, Hibernate, the JDK itself. Platform types are the crack through which the nulls that Kotlin’s compiler can’t detect slip in.
Ecosystem and tooling
Spring Boot: the success story
Spring Boot works well with Kotlin. There’s official support, extensions, and an active community. If your stack is Spring Boot, migrating to Kotlin is viable and the experience is good.
But “works well” doesn’t mean “works the same.” There are nuances:
- Open classes. Spring needs many classes to be open for proxying. Kotlin closes them by default. You need the
kotlin-springplugin to open them automatically. - Data classes as JPA entities. Hibernate needs no-arg constructors. Kotlin data classes don’t have them by default. You need the
kotlin-jpaplugin. - Annotations. Some Spring annotations don’t behave the same in Kotlin.
@RequestParamwith default values,@Valuewith properties, etc. They’re solvable, but they’re surprises that cost time.
Ecosystem libraries
The vast majority of JVM libraries are Java. They work from Kotlin, but the experience isn’t always idiomatic. There are Kotlin-first libraries (Ktor, Exposed, kotlinx.serialization), but they’re a smaller ecosystem.
| Aspect | Java | Kotlin |
|---|---|---|
| Available libraries | All | All Java ones + Kotlin-native ones |
| Documentation and examples | Abundant | Growing, but still less |
| Stack Overflow | Extensive | Considerable, but less |
| IntelliJ tooling | Excellent | Excellent |
| Gradle/Maven plugins | Mature | Mature (Gradle better than Maven) |
Hiring: the conversation nobody wants to have
This is the elephant in the room. If you’re deciding between Java and Kotlin for a new project, talent availability matters as much as language features.
The market reality
- Senior Java developers: Abundant. Easy to find. Wide range of experience.
- Senior Kotlin developers (backend): Fewer. Most come from Android. Those doing backend Kotlin are a smaller subset.
- Java developers willing to learn Kotlin: Many. But they need ramp-up time.
I’ve participated in hiring processes where the requirement was “Kotlin backend.” The candidate pool shrank to a fraction compared to “Java backend.” Not because Kotlin is unpopular, but because backend adoption is still lower than in Android.
The hidden cost
When you hire someone with Java experience but not Kotlin:
- The first 1-2 months they produce less.
- The code they write at first is “Java with Kotlin syntax” (not idiomatic).
- They need mentoring from someone on the team who does know Kotlin well.
- Code reviews are slower because the reviewer has to explain idiomatic patterns.
This isn’t an argument against Kotlin. It’s an argument for including ramp-up cost in the decision.
Mixed teams: Java and Kotlin in the same project
Java-Kotlin interoperability is real and it works. You can have Java modules and Kotlin modules in the same Gradle/Maven project. IntelliJ handles them without issues. But a mixed project has costs that aren’t technical.
What works
- Modules separated by language. The legacy module is in Java, new modules are in Kotlin. Each has its style, its compiler, its dependencies.
- Gradual migration. You convert Java classes to Kotlin as you touch them. IntelliJ has an automatic converter that, with manual review, works reasonably well.
What creates friction
- Two styles in the same repo. PRs mix Java and Kotlin. Developers have to master both to do reviews.
- Split conventions. Do utility functions go in companion objects or in top-level functions? Are new DTOs data classes or Java records? Is
!!allowed or rejected in review? - Build complexity. Compiling a mixed Java+Kotlin project is slower than pure Java or pure Kotlin. The Kotlin compiler needs to analyze Java classes and vice versa in a specific order.
// Kotlin calling Java
val config = JavaLegacyConfig.getInstance() // returns Config!
// Is it nullable? Depends on the Java implementation.
// Kotlin doesn't know. Neither do you without reading the Java code.// Java calling Kotlin
KotlinService service = new KotlinService();
String result = service.process(input);
// Can it return null? Depends on whether in Kotlin it's String or String?
// Kotlin generates @Nullable/@NotNull annotations, but not always.My recommendation for mixed teams
- Define a style guide that covers both Java and Kotlin. Which patterns are used in each language, what’s forbidden, how things are named.
- Don’t convert working Java code just for the sake of converting it. If a Java module is stable, tested, and nobody touches it, leaving it in Java is a valid decision.
- Kotlin for new code, Java for legacy maintenance. This is the pattern that generates the least friction in practice.
- Make sure the entire team can read both languages. They don’t need to be experts in both, but they do need to be able to do reviews.
Technical debt: the kind that matters isn’t about syntax
When someone proposes migrating from Java to Kotlin, the argument is usually “we reduce boilerplate, the code is cleaner, less technical debt.” And it’s partially true. Kotlin eliminates unnecessary verbosity. But the real technical debt of a project isn’t in the getters and setters.
The technical debt that matters is:
- Poorly designed architecture. A God Class service doesn’t improve by rewriting it in Kotlin.
- Lack of tests. Migrating to Kotlin without adding tests is changing the color of the debt, not reducing it.
- Coupling between modules. If your services are coupled, they’ll stay coupled in Kotlin.
- Code nobody understands. If a 200-line Java method is incomprehensible, an 80-line Kotlin method with nested scope functions can be just as bad.
Changing languages doesn’t reduce technical debt. Redesigning does. The language is a tool; technical debt is a design problem.
I’ve seen projects where the migration to Kotlin was used as an excuse to rewrite entire modules. And in some cases it worked, not because Kotlin was better than Java, but because the rewrite forced a redesign. But you could have done that in Java too.
My criteria for new projects
If I start a backend project today and have freedom of choice:
| Situation | My choice | Why |
|---|---|---|
| Team with Kotlin experience | Kotlin | You leverage null safety, conciseness, coroutines |
| Java-only team, long project | Java (with future Kotlin possibility) | You don’t pay ramp-up cost at the start |
| Small microservice, small team | Kotlin | Less code, smaller bug surface |
| Project with many Java legacy integrations | Java | Less friction, fewer platform types |
| MVP or quick prototype | Either | The language isn’t the bottleneck |
| Library that other teams will consume | Java | Maximizes compatibility |
What doesn’t show up in benchmarks
Java vs Kotlin benchmarks measure compilation performance, runtime performance, binary size. None of them measure what truly matters in the long run:
- How many hours it takes a new team member to become productive.
- How many bugs reach production due to poor null handling.
- How long a code review takes in a mixed project.
- How much it costs to find a candidate with experience in your stack.
- How many times someone broke something because they didn’t understand a chain of scope functions.
Those metrics are what determine whether Kotlin is the right decision for your team. And they depend on your context, not on language features.
Java is not an obsolete language. Kotlin is not a silver bullet. The best decision is the one your team can maintain for the next three years without accumulating invisible technical debt. Sometimes that’s Kotlin. Sometimes it’s Java. And sometimes it’s both, with a clear plan for coexistence.


