How integration between Kotlin and Spring worked in the past?
At the beginning of their integration, Kotlin and Spring boot integration faced a quite a number of challenges:
- Nullability gap – Integrating Kotlin and Java libraries, like the ones from the Spring ecosystem, resulted in problems with nullability. Before null-safety annotations were added to the Spring Framework, Kotlin compilers treated types received from Java as platform types, which basically means Kotlin could not apply its own null checks. This resulted in a lot of null-pointer exceptions which Kotlin was initially designed to eliminate, and the end result was losing null-safety.
- Problems with reflection – Spring uses a lot of reflection for dependency injection, and creation of proxies. At the beginning, Kotlin’s reflection solution was much slower than Springs, resulting in performance problems.
- Problem with Kotlin classes being final by default – even though this is a good practice (see book Effective Java, 3rd edition), this does not go well with frameworks like Spring, which relies heavily on the concept of proxies through usage of CGLIB, where CGLIB requires classes to be open for extension.
- Problems with data classes and JPA – The obvious idea of using Kotlin’s data classes in combination with Spring and Hibernate is causing problems since data classes have default equals(), hashCode() and toString() including all params and no no-args constructor, which causes a couple of issues (link):
- Data classes are not fulfilling JPA specification which states that entity class must have no-args constructor and must not be final.
- Data classes implements simple hashCode() based on final fields defined in primary constructor, and equals() and toString() also, which is not in accordance to the JPA Specification (best way to implement hashCode(), equals() and toString()), and can cause different issues when using Hibernate.
- Fetching unwanted lazy associations – again, due to default implementation of hashCode(), equals() and toString(), any call to these methods will cause fetching all associated objects.
Luckily, thanks to heavy collaboration between Kotlin and Spring teams, major pain points of integrating Spring with Kotlin have been solved by introducing dedicated compiler plugins (e.g. all-open plugin, no-args plugin), spring-null-safety improvements, etc. And ongoing collaboration between JetBrains and Spring is getting stronger and stronger (see Strategic partnership with Spring announced). Knowing those facts, time was ripe for us to embark ourselves on a journey with Kotlin.
But, why Kotlin?
There are a couple of Kotlin features that are worth highlighting:
Readability and conciseness
Kotlin’s syntax is significantly more concise, reducing boiler plate code and thus making the codebase cleaner and easier to read and maintain, making the project overall less error prone. Kotlin excels at this field very much, introducing concepts like function default parameters and named arguments, etc. The example demonstrates the usage of named arguments, where we are creating a NotificationService class, with method overloading in Java:
public class NotificationService {
public void sendNotification(String message, String recipient) {
sendNotification(message, recipient, "default_subject", false);
}
public void sendNotification(String message, String recipient, String subject) {
sendNotification(message, recipient, subject, false);
}
public void sendNotification(String message, String recipient, String subject, boolean highPriority) {
System.out.println("Sending: '" + message + "' to " + recipient + " (Subject: " + subject + ", High Priority: " + highPriority + ")");
}
}
// Usage in Java:
NotificationService service = new NotificationService();
service.sendNotification("Hello", "john_d@example.com");
service.sendNotification("Urgent!", "jane_d@example.com", "Urgent Alert", true);
Same class in Kotlin will look like:
class NotificationService {
fun sendNotification(
message: String,
recipient: String,
subject: String = "Default Subject", // Default value
highPriority: Boolean = false // Default value
) {
println("Sending: '$message' to $recipient (Subject: $subject, High Priority: $highPriority)")
}
}
// Usage in Kotlin:
val service = NotificationService()
service.sendNotification(
message = "Urgent!",
"jane@example.com",
highPriority = true
) // Named argument for clarity, uses default subject
service.sendNotification(
message = "FYI",
"bob@example.com",
subject = "Information"
) // Named argument for subject and priority, uses default recipient
Clearly, the Kotlin version is much easier to read and more elegant.
Smart casts
Kotlin’s compiler is intelligent enough to automatically cast variables to a specific type after a type check, eliminating redundant explicit casts common in Java. This leads to cleaner and safer code.
public void processObject(Object obj) {
if (obj instanceof String) {
String s = (String) obj; // Explicit cast needed
System.out.println("String length: " + s.length());
}
}
But in Kotlin,
fun processObject(obj: Any) { // Any is Kotlin's root type, like Object in Java
if (obj is String) { // 'is' operator checks type
println("String length: ${obj.length}") // No explicit cast needed!
}
}
Null safety in Kotlin
One of Kotlin’s great features (arguably, null-safety is the best Kotlin feature) is its built-in null safety, which helps eliminate well known, infamous, NullPointerExceptions, by enforcing checks at compile time. This is a game-changer for writing more robust and reliable applications. Even though Java is moving forward in null-safety introduction (apparently it is planned as a preview feature for Java 27), it will still take a lot of work and time to get this feature running.
Kotlin has two types of references that are interpreted by the compiler, nullable and non-nullable references. By default, Kotlin compiler assumes that the value of reference can not be null.
In case of those references, if one tries to assign a null value to it, it will cause a compiler error.
For creating a nullable reference, one needs to add a question mark(?) to the type definition.
When we are working with nullable references, we need to provide safe calls in order to avoid compilation errors.
Kotlin solved it in the most elegant way, introducing a syntax for safe calls – this syntax allows programmers to execute an action only when the specific reference holds a non-null value.
In the following example, we will demonstrate Kotlin null safety:
// Non-nullable value
var firstName: String = "John"
firstName = "Jena"// Works, assigning another value
firstName = null // X - Will not work, compiler will complain
// Nullable value
var lastName: String? = "Doe"
lastName = null // Works, it is nullable reference
// Safe calls
data class Address(val postalCode: String?)
val address: Address? = Address("1010")
val myPostalCode = address?.postalCode
In situations when we have a reference, and we want to return some default value from the operation if the reference holds a null, we can use an elvis (?:) operator. This operator is equivalent of orElse/orElseGet from Java Optional class:
val firstName: String? = null // Elvis operator ?:, since here value is null, valueLength is going to be -1 val valueLength = firstName?.length ?: -1
Why does Kotlin work well with Spring Boot?
The Spring Boot synergy
Spring Boot, with its opinionated approach to application development, complements Kotlin beautifully. We are going to demonstrate with a couple of simple examples, why this pairing has been so successful for us:
- First-class Kotlin support: Spring Boot has been really open to Kotlin, providing dedicated support that makes working with the two feel incredibly natural, enabling Kotlin DSL for Spring configuration and enhanced auto-configuration for Kotlin projects streamline development.
- Spring Boot Controller in Kotlin:
// Assuming a User data class and UserRepository exists
@RestController
@RequestMapping("/api/users")
class UserController(private val userRepository: UserRepository) {
@GetMapping
fun getAllUsers(): List = userRepository.findAll() // findAll() in Spring Data JPA is considered to be anti-pattern, here it is used for a sake of example
@GetMapping("/{id}")
fun getUserById(@PathVariable id: Long): User =
userRepository.findById(id).orElseThrow { RuntimeException("User not found") }
@PutMapping("/{id}")
fun updateUser(@PathVariable id: Long, @RequestBody user: User): ResponseEntity {
val existingUser = userRepository.findById(id) .orElseThrow { RuntimeException("User not found with id $id") }
existingUser.name = user.name
existingUser.email = user.email
val updatedUser = userRepository.save(existingUser)
println("Updated user with ID: ${updatedUser.id}")
println("New Name: ${updatedUser.name}")
println("New Email: ${updatedUser.email}")
return ResponseEntity.ok(updatedUser)
}
Notice how concise and expressive this is, leveraging implicitly generated getters/setters and direct constructor injection.
- Reduced boilerplate with Spring and Kotlin: Both Spring and Kotlin aim to reduce boilerplate. When combined, this effect is amplified. For instance, Spring’s dependency injection combined with Kotlin’s concise syntax means you write less code to achieve the same functionality.
- Active community and rich ecosystem: Both Kotlin and Spring boast large, active communities and rich ecosystems. This means access to a wealth of resources, libraries, and community support, which is invaluable when tackling complex development challenges.
Success factors for adopting Kotlin
Adopting a new programming language or technology, even one with clear benefits for us, like Kotlin, does not come without a learning curve and requires planning. Some of the key factors, based on my team experience that significantly contribute to a successful transition:
- Existing internal expertise : Having colleagues in the company who already possess experience in Kotlin is really valuable. Colleagues already experienced with the technology that you want to learn can:
- Accelerate learning: Provide direct mentorship, answer questions quickly, and offer real-world insights that go beyond documentation.
- Establish best practices: Help in defining coding standards, architectural patterns, and effective ways to use Kotlin within your specific project context.
- Build confidence: Demonstrate the feasibility and benefits of the language through their own successful implementations, fostering buy-in from other team members and management.
- Troubleshoot and debug: Offer support in navigating common pitfalls and complex issues that newcomers might face.
- Access to quality learning resources: Beyond internal expertise, readily available and high-quality external learning resources are crucial. This includes:
- Official documentation
- Online courses and tutorials: Platforms like Coursera, Udemy, Pluralsight, and even free resources on YouTube offer structured learning paths.
- Books: For deeper dives into advanced topics, design patterns, and idiomatic Kotlin.
- Community forums and Q&A sites: Stack Overflow and Reddit communities (like r/Kotlin) provide excellent opportunities to ask questions and learn from others’ experiences.
- Starting small and iterating
- Smaller project / or feature: Begin with a small, self-contained project or a new feature where the risk is low. This allows the team to gain experience without impacting critical systems.
- Incremental migration: For existing Java codebases, leverage Kotlin’s excellent interoperability with Java. You can gradually introduce Kotlin files into a Java project, rewriting parts as needed, rather than a “everything -at – once” rewrite.
- Learn from early mistakes: The initial projects will represent a challenge, introducing struggle and mistakes. Use these as learning opportunities to refine your process and understanding.
- Clear use cases and benefits: The adoption will be smoother if the team and stakeholders clearly understand why Kotlin is being introduced.
- Improved productivity: Highlight how Kotlin’s conciseness and expressive power can lead to faster development.
- Enhanced code quality: Emphasize features that reduce common bugs.
- Better maintainability: Discuss how clean, idiomatic Kotlin code is easier to read and maintain in the long run.
- Modern language features: Showcase how features like coroutines for asynchronous programming simplify complex tasks.
Our journey so far
Our transition to using Kotlin with Spring Boot has been moderately smooth. The team quickly adapted to Kotlin’s syntax and paradigms, and the benefits were almost immediately apparent. Where we can say that we improved:
- Quick adoption: Since we are using Kotlin on top of Spring, our engineers were able to quickly and painlessly switch to Kotlin. By getting familiar with good Kotlin features, motivation and productivity grew.
- Faster development cycles: The conciseness and expressiveness of Kotlin, combined with Spring Boot’s rapid development features, have significantly sped up our development cycles.
- Fewer runtime errors: Kotlin’s null safety and strong type system have noticeably reduced the number of runtime errors, leading to more stable applications, with less errors and bugs.
- Improved code maintainability: The cleaner, more readable code has made it easier to onboard new team members and maintain our codebase in the long run.
Kotlin in our future? Oh yes, we’re just getting started! 🚀
