Why You Should Migrate Microservices From Java to Kotlin: Experience and Insights
Learn why migration to Kotlin is so successful and why developers are eager to switch to this language, even with prior experience only in other JVM languages.
Join the DZone community and get the full member experience.
Join For FreeI work at one of the largest private banks in Eastern Europe, developing the backend for a mobile application. Our cluster consists of more than 400 microservices, and peak loads on individual services can reach five-digit values. When we initially started transitioning to a microservices architecture, all our code was written in Java. However, over time, we began actively migrating microservices to Kotlin. Today, all new microservices are created exclusively in Kotlin, and the share of Java code has decreased to less than 20%.
In this article, I will explain why the migration to Kotlin has been so successful and why developers are eager to switch to this language, even with prior experience only in other JVM languages.
Kotlin vs Java
One of Kotlin’s main strengths is its full compatibility with Java code. Kotlin seamlessly interacts with Java classes and methods, and vice versa, allowing for a smooth transition to the new language without the need to rewrite existing code. Kotlin uses JVM bytecode, ensuring compatibility with all the Java libraries and frameworks that we actively use in our project.
The modular structure of microservices is ideal for the gradual introduction of Kotlin. We started by developing new components in Kotlin, then slowly migrated older ones. Importantly, we could continue using Java code without worrying about conflicts or breakdowns. This approach allowed us to avoid significant risks and ensured the stable operation of the entire cluster.
Kotlin offers a range of modern features that simplify development and make it more efficient. Here are a few examples:
- Coroutines: Enable easy and efficient writing of asynchronous code, avoiding the complexity of Java's multithreading
- Extensions: Allow adding new functions to existing classes without modifying them
- Nullable types: Solve the issue of
NullPointerException
, a common problem for Java developers
Switching to Kotlin is smooth and natural because its syntax closely resembles Java. However, unlike Java, Kotlin eliminates much of the boilerplate, making the code more concise and readable. For instance, creating POJO classes in Kotlin can be done in a single line using data class
. Similar improvements can be seen in other aspects of the language, such as working with collections, asynchronous programming, and handling nullable types.
Code Examples: Java vs. Kotlin
To illustrate, here are a few comparisons:
POJO Class vs. Data Class
public class Person {
private String name;
private int age;
/*
Also here you need to add a constructor,
getters and setters for each field,
toString,
equals,
hashCode
*/
}
In Java, creating a simple POJO (Plain Old Java Object) requires explicitly defining fields, constructors, getters, setters, and often overriding toString()
, equals()
, and hashCode()
methods. This results in a lot of boilerplate code.
data class Person(val name: String, val age: Int)
Kotlin's data class
automatically generates constructor, getters (and setters for var properties), toString()
, equals()
, hashCode()
, and copy()
methods. This single line of code is equivalent to dozens of lines in Java.
Nullable Types
String name = null;
if (name != null) {
System.out.println(name.length());
} else {
System.out.println("Name is null");
}
In Java, you need to explicitly check for null before accessing an object to avoid NullPointerException
. This often leads to verbose null-checking code.
val name: String? = null
println(name?.length ?: "Name is null")
Kotlin uses the safe call operator (?.)
and the Elvis operator (?:)
to handle nullables concisely. This line safely accesses the length if name
is not null, or otherwise prints "Name is null"
. In Kotlin, the safe call operator, ?.
, and the ?:
operator are used for default values, simplifying null handling.
Nested Object Checks
if (person != null && person.getAddress() != null && person.getAddress().getCity() != null) {
System.out.println(person.getAddress().getCity());
}
In Java, checking nested objects for null
requires multiple checks, leading to deeply nested if statements or long boolean expressions.
person?.address?.city?.let {
println(it)
}
Kotlin's safe call operator (?.)
allows for chaining nullable calls. The let
function executes the block only if all the previous calls in the chain are non-null.
Asynchronous Calls
public Mono<String> fetchData() {
Mono<String> first = fetchFirst();
Mono<String> second = fetchSecond();
return Mono.zip(first, second)
.map(tuple -> tuple.getT1() + " " + tuple.getT2());
}
This Java code uses Project Reactor's Mono
for asynchronous programming. It fetches two pieces of data asynchronously and combines them.
suspend fun fetchData() {
val first = async { fetchFirst() }
val second = async { fetchSecond() }
println("${first.await()} ${second.await()}")
}
Kotlin's coroutines make asynchronous code look and behave like synchronous code. The async function starts coroutines for fetching data, and await()
suspends execution until the result is available.
Collection Processing
List<String> names = Arrays.asList("John", "Jane", "Doe");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.map(String::toUpperCase)
.collect(Collectors.toList());
Java uses streams and lambda expressions for collection processing. This code filters names starting with "J"
and converts them to uppercase.
val names = listOf("John", "Jane", "Doe")
val filteredNames = names.filter { it.startsWith("J") }
.map { it.uppercase() }
Kotlin provides concise functions for collection processing. The code does the same as the Java version but with a more readable syntax. Kotlin offers a more concise and expressive syntax for working with collections.
If-Else in Java and When in Kotlin
int number = 3;
String result;
if (number == 1) {
result = "One";
} else if (number == 2) {
result = "Two";
} else {
result = "Other";
}
Java uses traditional if-else
statements for conditional logic. This can become verbose with multiple conditions.
val number = 3
val result = when (number) {
1 -> "One"
2 -> "Two"
else -> "Other"
}
Kotlin's when
expression provides a more concise and powerful replacement for switch statements and complex if-else
chains.
Class Extensions
public class StringUtils {
public static String reverse(String s) {
return new StringBuilder(s).reverse().toString();
}
}
In Java, adding functionality to existing classes often requires creating utility classes with static methods.
fun String.reverse(): String = this.reversed()
Kotlin's extension functions allow adding new methods to existing classes without modifying their source code. This reverses a string and can be called directly on String objects.
val reversed = "Hello".reverse() // "olleH"
The extension function can be called as if it were a method of the String class, making the code more intuitive and object-oriented.
These examples clearly show how Kotlin simplifies and enhances development compared to Java through concise syntax, built-in features, and powerful tools for asynchronous programming and collection handling.
Conclusion
Speaking of the compatibility of Java and Kotlin within the same project, I want to emphasize that using microservices in both languages does not require creating separate libraries and starters. The only limitation is the Java version, which must not be higher than the one used in the services. Spring also fully supports both languages, and the difference in use lies only in some dependencies that need to be pulled in through Maven or Gradle build systems.
After several years of using Java and Kotlin simultaneously on our project, I can confidently say that developers do not want to go back to Java. They like Kotlin much more — it is more concise and expressive and provides more opportunities for writing efficient code. Kotlin code is easier to read and understand, especially when regularly reviewing your colleagues' pull requests. For programmers with experience in other JVM languages, the transition to Kotlin is very fast.
Of course, Java is not standing still. In the new versions, features like records have appeared, which serve as an analog of data classes in Kotlin. Virtual threads, which could replace coroutines, are also in development. Additionally, work is underway to improve the support for nullable objects.
Nevertheless, most of our developers prefer to work with Kotlin. They appreciate its conciseness, expressiveness, and modern features that significantly improve productivity and code quality. The transition from Java to Kotlin has been simple and natural for us, allowing us to maintain the existing system and gradually introduce the new language.
Opinions expressed by DZone contributors are their own.
Comments