Ever heard of “Delegation” or “Value Classes” in Kotlin? If not, you’re missing out on some powerful features that can make your development smoother and more efficient.
Throughout my career, I’ve made and reviewed thousands of pull requests. While the code is often clean and functional, I’ve noticed it could be even better if developers were more familiar with some of Kotlin’s advanced language features and libraries.
Many developers transitioning from Java (or other JVM-based languages) to Kotlin follow a natural progression—one that I, too, have experienced. However, there are features, syntactic sugars, and conventions that often go unnoticed.
I’ve been compiling a growing list of these overlooked aspects, and I’ve decided it’s time to share it. I’ll first mention the items applicable anywhere in your codebase, from the easiest to apply to the most expensive. Then, I’ll cover a couple of items that are exclusive to test code.
- Variables and Immutability: Use
val
overvar
, data classes, and immutable collections for safer, more predictable code. - Inline Value Classes: Boost performance by wrapping primitives or single-property classes without the overhead of a full class.
- Better Practices in Scope Functions: Achieve more readable and cleaner code using
let
,run
, andalso
. - Notes on Null Safety Operators: Master
?.
,!!
, and?:
for better null safety. - Delegations: Reduce boilerplate by delegating parts of your implementation to other classes or properties.
- Forever Now!: Keep your tests consistent by freezing time.
- JSON Assertions: Make your tests cleaner and more reliable with JSON-based assertions.
- and more!
Variables and Immutability
I find it delightful to emphasize the importance of immutability. Whenever possible (almost everywhere), avoid mutable variables to promote thread safety, predictability, and clear intent.
val
, var
dilemma
I’ve often seen developers choose var
when they could have used val
.
While variables are one of the first concepts we learn in programming, the idea of constants or values (val
in Kotlin) is often overlooked.
Avoid using var
as much as possible. and use val
instead as it brings immutability with no cost.
Data Classes
As of Java 17 (originally in version 14), you have the luxury of declaring record
s. But in the Kotlin world, this concept was introduced in the initial phases of the language’s design. Data Classes reduce boilerplate code (or injecting dependencies like Lombok
) and make your instances with the following functionalities:
equals()
/.hashCode()
pair. This assures you it compares the content when using the equals sign or function instead of reference comparison.toString()
of the formPerson(name=John, age=42)
.copy()
, One of my favorites as it promotes immutability since you can copy a specific object and only assign the properties needed.
val jack = Person(name = "Jack", age = 1) val olderJack = jack.copy(age = 2) jack.toString() // Person(name=Jack, age=1) olderJack.toString() // Person(name=Jack, age=2)
Data Class Considerations
It is important to understand that the functionalities mentioned above only consider the properties defined on the constructor of the data class
.
This means the following code might behave against the expectations of some people:
data class Person(val name: String, val age: Int) { var mood = "normal" } val jack = Person(name = "Jack", age = 1) val upsetJack = jack.copy() upsetJack.mood = "upset" jack.toString() // DOES NOT serialize the 'mood' property jack == upsetJack // computed to TRUE
Immutable Collections
In Kotlin, collections such as List
are immutable by default. This means they don’t provide functions such as add
or remove
, and when you need to add or remove items from them, you need to create new collections.
This is quite handy since it promotes immutability, which leads to better predictability and clear intent.
However, sometimes your algorithm and logic force you to work with mutable collections.
In such cases, you can use the mutable version of the collection.
An example is MutableList
, which does provide functionalities to modify the list.
Inline Value Classes
When a property has specific meaning, it’s better to reflect this fact in your code, whether you’re following Domain-Driven Design (DDD) principles or not. Inline Value Classes in Kotlin can help you achieve this.
Why Not Use Simple Primitive Types?
Primitive types work, but they don’t restrict invalid values. It’s better to use types that enforce business rules and convey meaning.
Why Not “Wrap It Inside a Class”?
Wrapping a value inside a class introduces runtime overhead, such as additional heap allocations. More critically, you lose runtime optimizations, especially if the value is a primitive type.
Why Not Use “Type Alias”?
The main issue with type aliases is that they remain assignment-compatible with their underlying type.
Additionally, type aliases do not allow you to implement domain-related logic, such as validation during construction or conversion.
Inline Value Classes in Practice
Let’s say you’re building a CMS for a college where student IDs are properties of several entities. The IDs follow this format:
[major_code{2}][freshman_year{2}][random_number{3}]
You can now define the following value class:
@JvmInline value class StudentId(val id: Int) { init { require(isValid(id)) { "Student Id '$id' is not valid" } } companion object { private fun isValid(id: Int) = id in 1000000L..9999999 } } data class Student ( val id: StudentId, val name: String, )
Inline Value Class Considerations
Usage in Java
If you define a value class in Kotlin and want to use it in your Java code, you’ll need to apply the @JvmName
annotation to disable name mangling.
Name mangling is Kotlin’s mechanism for ensuring the JVM can differentiate between the value class and its underlying type, allowing for function overloading and other features.
Spring, JPA, and many other Java frameworks and libraries, process Kotlin’s compiled code. By default, the names of properties are not the exact names you assign; they include a name-hashcode
(e.g. {... "id-9ycq1": 1234567 ...}
instead of {... "id": 1234567 ...}
) due to the name mangling of the get
function.
This means you might run into some issues when working with Inline Value Classes. Namely:
Jackson
Jackson is a widely used Object Mapper in many Java frameworks such as Spring. This Java library might not work perfectly with Inline Value Classes without the required configuration or dependencies.
The problem, of course, comes from name mangling, as by default, the names of properties of value class types have an additional hash value.
This issue can be mitigated by adding the jackson-module-kotlin dependency.
Your application would work fine after adding this dependency, but you must employ the Jackson Object Mapper from the Kotlin module com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
instead of com.fasterxml.jackson.databind.ObjectMapper
.
The Gson library doesn’t have this caveat.
JPA and Hibernate
Be careful when using value classes in JPA entities.
It should work by default unless you need to introduce a nullable property of type Value Class.
In this case, you need to ensure that you introduce a converter for the nullable type.
For example, consider the following piece of code:
@Entity data class Student ( @Id val id: Long, @Column val studentId: StudentId?, @Column val name: String, )
In which StudentId
is a value class. This runs into an error that looks like the following:
... org.hibernate.type.descriptor.java.spi.JdbcTypeRecommendationException: Could not determine recommended JdbcType for Java type '....StudentId' ...
The solution would be to introduce a @Converter
:
@Entity data class Student ( @Id val id: Long, @Column @Convert(converter = NullableStudentIdConverter::class) val studentId: StudentId?, @Column val name: String, ) @Converter class NullableStudentIdConverter: AttributeConverter<StudentId?, Int?> { override fun convertToDatabaseColumn(studentId: StudentId?) = StudentId?.id override fun convertToEntityAttribute(id: Int?) = id?.let { StudentId(it) } }
Swagger OpenApi Documentation
As Swagger is implemented in Java, in your auto-generated documentation, you might encounter the mangled version of the names of the functions having a Value Class as an input.
This happens for the operationId
in the schema specification, which can be addressed by overriding the operationId from the @Operation
annotation whenever needed.
There would be more items in this list, but the rule of thumb would be to:
- Make sure you use the Kotlin-friendly modules whenever provided.
- Keep an eye on the name mangling mechanism and its side effects.
Better Practices in Scope Functions
The practice of chaining gained popularity with the introduction of Java Streams. In Kotlin, you can use chaining and scope functions to create distinct code blocks, improving readability and reducing the risk of certain human errors. However, sometimes we aren’t fully familiar with all of these functions and their potential.
There’s almost no way to overuse scope functions, so if a task can be accomplished using them, it’s usually a good idea to do so.
.let
and .run
These two scope functions are well-known, yet they are often underutilized.
A common pattern where they are frequently overlooked is when a function has multiple exit points.
Although having multiple exit points in a function is sometimes unavoidable, it increases the cognitive load on the reader, who must trace through the code to ensure all scenarios are covered.
Using .let
or .run
can help streamline this logic. Consider the following example:
fun calculateAge(student: Student?): Period? { if (student == null) return null return LocalDate.now().until(student.dateOfBirth) }
Versus when let
is employed:
fun calculateAgeWithLet(student: Student?) = student?.let { LocalDate.now().until(it.dateOfBirth) }
Or alternatively when run
is employed:
fun calculateAgeWithLet(student: Student?) = student?.run { LocalDate.now().until(this.dateOfBirth) }
I find the latter versions much more concise and readable.
.also
Sometimes you need to perform an auxiliary task, such as logging or persisting an audit log, right before your code block ends. While you could save the return value, perform the task, and then return the value, it’s much easier to use the .also
function. This extension function allows you to execute the task without affecting the return value, making your code cleaner and more efficient.
fun calculateAgeWithLet(student: Student?): Period? = student?.let { LocalDate.now().until(it.dateOfBirth) }?.also { writeToLog("Student '$student' is '$it' old.") }
.apply
apply
is an extension function that can reassign variables of an object and return the modified version.
It is highly used for object configuration.
For example:
val mapper = jacksonObjectMapper().apply { val module = KotlinModule() module.registerSerializer(Student::class, CustomStudentSerializer()) registerModule(module) } mapper.writeValueAsString(student)
In the example above, the mapper object is first created, and then the configuration is applied to it in its specific block. In this block of code, the reference object this
refers to the instance of jacksonObjectMapper
.
Scope Function Considerations
Nesting
Nesting scope functions is generally discouraged. When you nest these functions, the it
or this
references may change meaning within different nested blocks, which reduces readability and increases the risk of bugs due to human error.
Be Consistent
Chaining run
with let
or similar can be confusing due to different reference usages (this
vs. it
). Stick to one style for better readability.
Non-Extension run
There are two versions of the run
function in Kotlin. One is a non-extension function, meaning it can be used at the beginning of a block without requiring an object reference:
run { // some work gets done here. this // might not exist or refer to the same object as outside of the block. }
The other run
function is .run
, which is an extension function that uses this
as the reference within its block.
Notes on Null Safety Operators
The Elvis Operator
The Elvis operator ?:
is a powerful tool for handling null conditions in your code.
It improves readability by making your code more concise while ensuring that operations or functions return a meaningful non-null value.
Consider the following assignment:
val dateOfBirth: Period = if(student != null) student.dateOfBirth else throw IllegalStateException("some meaningful error message here")
This can be rewritten as:
val dateOfBirth = student?.dateOfBirth ?: throw IllegalStateException("some meaningful error message here")
Which is more readable, concise and to the point.
The !!
Operator in Your Main Code
Apologies to the NPE11-lovers, but I recommend banning the use of this operator in your main code (not in tests, as test code is supposed to be completely deterministic).
Instead, use the Elvis operator or other methods to handle nullability.
The reason is that you should control the nullability of your object/value at the point of declaration.
If that’s not an option, return or throw meaningful custom messages when encountering null states.
The !!
operator attempts to ensure that the optional value is non-null, but if it isn’t, it throws the infamous NullPointerException
.
By that time, it’s too late—you should have handled nullability before reaching this line of code!
Safe Casts
In Kotlin, you can use the reserved word is
to check whether an object is of a type or not.
You can also use the as
reserved word to cast an object to a specific type if the type is a supertype.
For example:
val person: Person? = if (student is Person) student as Person else null
You can rewrite the code above to:
val person: Person? = student as? Person
I find the latter version much more readable and, crucially, less vulnerable to human errors as you are not repeating the object and type over and over.
Delegations
One of Kotlin’s powerful features that I wish I had discovered sooner is delegation, achieved using the by
keyword.
Delegation is useful in many scenarios. For example, if you’re using Spring JPA (Java Persistence API) and define a JpaRepository
, the interface, typically implemented by a JPA provider like Hibernate, already covers many of your needs. However, suppose you want to add custom functions to this repository. In Kotlin, interfaces can implement their own functions, unlike in Java.
The challenge is that the JPA repository expects function names to correspond meaningfully to the underlying entity and its properties, and it tries to implement them automatically. This can lead to compile-time errors. You have at least two options:
Add a Component: Create a separate component that internally calls the necessary functions from the repository. However, this approach requires adding an extra line of code every time you need to call a new function.
Use Delegation: Create a component that inherits the repository interface and delegates the implementation of the
JpaRepository
to Hibernate. With Kotlin’s delegation feature, you can build a Spring component that provides all the functionality of the repository interface while adding new functionality. Here’s an example:
@Repository interface StudentRepository : JpaRepository<Student, Int> @Component class StudentExtendedRepository(jpaRepo: StudentRepository) : StudentRepository by jpaRepo { fun sayHello() = findAll().map { "Hello ${it.name}" } } ... // This works: studentExtendedRepository.sayHello() // So does this: studentExtendedRepository.findAll()
Property Delegation
Delegations alone and especially Property Delegation deserve its own article as it has lots of variants and use cases.
I highly recommend reading the official documents about it.
Here, I would like to mention a couple of the property delegations that I find quite interesting.
Observable Properties
You can add the observable
standard delegate to log changes on a property or apply necessary operations after a change is applied.
If you need to intercept the new value and override it before assignment, you should use the vetoable
standard delegate instead.
class User { var name: String by observable("<no name>") { prop, old, new -> println("$old -> $new") } } fun main() { val user = User() user.name = "first" // prints <no name> -> first user.name = "second" // prints first -> second }
Delegating to Another Property
Here I would say no more but to point you to the code example from the official documents.
The code is so simple and comes with well-named values, I think it would be better to leave the explanation to Kotlin instead of English.
var topLevelInt: Int = 0 class ClassWithDelegate(val anotherClassInt: Int) class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) { var delegatedToMember: Int by this::memberInt var delegatedToTopLevel: Int by ::topLevelInt val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt } var MyClass.extDelegated: Int by ::topLevelInt
I find delegation to another property in the same class very useful in cases where you are marking a specific property as deprecated but still need to hold onto it for a while.
Your codebase doesn’t need to take care of the deprecated property; rather, you can simply indicate that it reads the value from the new one.
Delegation Considerations
Be Mindful of Who Implements What
When transitioning from traditional inheritance, it’s easy to fall into a common trap:
assuming that when your derived class or interface overrides a val
or fun
, other functions in the base class will automatically use the overridden version.
However, with delegation, non-overridden functions in the base class continue to use their original implementation, as they are independent objects.
This can lead to unexpected behavior, so it’s quite important to understand how delegation works in this context. To better understand this, consider the following example:
interface Base { val message: String fun print() { println(message) } } class Derived(b: Base) : Base by b { override val message = "Derived" } fun main() { val b = object : Base { override val message = "Base" } val derived = Derived(b) derived.print() // prints > Base println(derived.message) // prints > Derived }
Forever Now!
Check out the Kotest extensions, and I guarantee you’ll find at least one interesting tool you can use in your project.
One standout is the Instant extension, which replaces the now
static functions in java.time
to make your tests more predictable.
Here’s an example from the Kotest extensions documentation:
val foreverNow = LocalDateTime.now()
withConstantNow(foreverNow) {
LocalDateTime.now() shouldBe foreverNow
delay(10) // Code is taking a small amount of time to execute, but now
changed!
LocalDateTime.now() shouldBe foreverNow
}
Forever Now Considerations
Race Conditions
This extension is highly sensitive to race conditions because it overrides the static now
method, which is global to the entire JVM instance. If you’re running tests in parallel while using this extension, the results may be inconsistent.
Race Conditions Again
Be aware that time-dependent race conditions in your code might behave very differently when time doesn’t progress as expected. Additionally, if your logic requires different now
values during its lifecycle, this extension may not be the best choice.
Opening Packages with --add-opens
When using this extension in Kotlin, you might encounter an error like the following:
... Request processing failed: java.lang.reflect.InaccessibleObjectException: Unable to make private static int java.time.OffsetDateTime.compareInstant(java.time.OffsetDateTime,java.time.OffsetDateTime) accessible: module java.base does not "opens java.time" to unnamed module ...
This error indicates that the extension cannot replace the now
functionality because the java.time
package isn’t open. The simplest way to resolve this is to make the package open using Java arguments. For example, in Gradle Kotlin, you can add:
tasks { test { jvmArgs( "--add-opens", "java.base/java.time=ALL-UNNAMED", ) } }
JSON Assertions
One of the coolest matcher modules in the Kotest framework is the JSON Matcher. When writing API or integration tests, you often need to ensure that a JSON string’s contents are accurate. This set of matchers provides powerful tools to verify that an entire JSON string, or just a portion of it, matches an expected JSON structure. Two particularly useful matchers are:
shouldEqualJson
: Verifies that a string matches a given JSON structure.shouldEqualSpecifiedJson
: Verifies that a string matches a given JSON structure but allows additional unspecified properties.
There is more!
The list could go on, but I want to name some of the features briefly as they are quite interesting to look at. I won’t attempt to cover the entire topics here. They do deserve their own dedicated articles.
- Coroutines and Reactive Programming in Kotlin.
- Ktor: Create asynchronous client and server applications.
- Kotlin Native: Make native executables for Windows/Linux/macOS instead of using JVMs.
- Dokka: An API Documentation Engine for Kotlin.
- Inline functions: Eliminate the overhead to call functions by inlining lambda expressions and more!
- …
Conclusion
Kotlin offers a wealth of powerful features that can greatly enhance your coding experience from the importance of immutability, and inline value classes to the powerful delegation mechanisms and scope functions.
I think by adopting these techniques, you can write cleaner, more efficient, and more maintainable code.
Don’t be afraid to dive into these tools and integrate them into your projects.
You might be surprised at how much they can improve both your code quality and productivity.
Keep exploring, and happy coding!
NullPointerException ↩