Kotlin
https://kotlinlang.org/docs/home.html
Kotlin usage feedback
Learning
There are lots of tutorials out there, here are the ones that we read:
- Kotlin Programming Language
- kotlin koan
- https://github.com/alidehkhodaei/kotlin-cheat-sheet
- https://devhints.io/kotlin
- https://quickref.me/kotlin.html
Obviously, we leverage AI to help us understand and review our kotlin code:
- use
@claudeon PR to review and improve our code - create a system prompt / slash command to have a kotlin mentor
The good
Function / Method definition
In kotlin, function declarations are more natural and readable because we list the parameters (inputs) first, then the return type (output). This matches how we think: “Given these inputs, what output do I produce?“. In java, the return type comes first, which can feel less intuitive.
Optional<User> findById(UserId userId);fun findById(userId: UserId): User?Lenient trailing commas
kotlin allows trailing commas when listing arguments in function calls, collections, and class constructors. This means you can safely add a comma after the last item, which is especially useful when adding or removing lines.
Subscription(
id = SubscriptionId.create(),
owner = providerSubscription.owner,
beneficiaries = beneficiaries,
)java does not allow trailing commas… only in array initializers.
Safe call operator
https://kotlinlang.org/docs/null-safety.html#safe-call-operator
The safe call operator ?. allows you to handle nullability safely in a shorter form.
Instead of throwing an NullPointerException, if the object is null, the ?. operator simply returns null:
val a: String? = null
print(a?.length) // null instead of NPESafe calls are useful for chaining calls.
val name = alice?.department?.head?.nameIn java, there are no such things:
String name = alice != null && alice.getDepartment() != null && alice.getDepartment().getHead() != null
? alice.getDepartment().getHead().getName()
: null;
// or with `if/else`
String name = null;
if (
alice != null
&& alice.getDepartment() != null
&& alice.getDepartment().getHead() != null
) {
name = head.getName();
}The ruby version is similar to kotlin:
name = alice&.department&.head&.nameElvis operator
https://kotlinlang.org/docs/null-safety.html#elvis-operator
When working with nullable type, you can provide an alternative value if it’s null, for example:
fun displayName(user: User?): String {
return user?.name ?: "Guest"
}It can seamlessly be used with the safe operator, e.g. in an authentication service where we would want to extract the JWT and parse the user id from it:
fun getJwtToken(): String?
fun extractUserId(token: String): String?
fun getCurrentEndUserResult(): AuthResult {
return getJwtToken()
?.let { token -> extractUserId(token) }
?.let { userId -> AuthResult.Success(EndUser.from(userId)) }
?: AuthResult.Error("Absent or invalid or unauthenticated JWT")
}The java version would be:
Optional<String> getJwtToken();
Optional<String> extractUserId(String token);
AuthResult getCurrentEndUserResult() {
return getJwtToken()
.flatMap(token -> extractUserId(token))
.map(userId -> new AuthResult.Success(EndUser.from(userId)))
.orElseGet(() -> new AuthResult.Error("Absent or invalid or unauthenticated JWT"));
}It’s basically the same, but we have to wrap the String into a container in order to have something monadic.
That means, if we are dealing with a library that gives an API that returns null and not Optional, then to simulate such behavior, you have to wrap into an Optional, which is not really nice:
// Is this:
var foo = Optional.ofNullable(anApiThatReturnsNull())
.orElse("something else");
// better than this?
var foo = anApiThatReturnsNull()
if (foo == null) foo = "something else"Moreover, to ensure the compiler knows if we are dealing with null instances, we have to add tools, like JSpecify, whereas in kotlin, it’s native.
The ruby version would be:
def get_current_end_user_result
get_jwt_token
&.then { |token| extract_user_id(token) }
&.then { |user_id| AuthResult::Success.new(EndUser.from(user_id)) }
|| AuthResult::Error.new("Absent or invalid or unauthenticated JWT")
endHere’s another example where the elvis operator really shines:
We have a class that loads some configuration based on priority: user config takes precedence, then application and then default config.
class ConfigManager(
private val userConfig: Config?,
private val appConfig: Config?,
private val defaultConfig: Config
) {
fun getTimeout(): Int {
return userConfig?.timeout
?: appConfig?.timeout
?: defaultConfig.timeout
?: 30_000
}
fun getDatabaseUrl(): String {
return System.getenv("DB_URL")
?: userConfig?.dbUrl
?: appConfig?.dbUrl
?: "jdbc:h2:mem:test"
}
}The kotlin version is quickly readable and we can understand what’s going on at a glance.
Here’s the java version:
public class ConfigManager {
private Config userConfig;
private Config appConfig;
private Config defaultConfig;
public int getTimeout() {
if (userConfig != null && userConfig.getTimeout() != null) {
return userConfig.getTimeout();
}
if (appConfig != null && appConfig.getTimeout() != null) {
return appConfig.getTimeout();
}
if (defaultConfig.getTimeout() != null) {
return defaultConfig.getTimeout();
}
return 30_000;
}
// Or with Optional (still verbose and note necessarily more readable)
public String getDatabaseUrl() {
return Optional.ofNullable(System.getenv("DB_URL"))
.orElse(Optional.ofNullable(userConfig)
.map(Config::getDbUrl)
.orElse(Optional.ofNullable(appConfig)
.map(Config::getDbUrl)
.orElse("jdbc:h2:mem:test")));
}
}It’s more verbose and less readable than the kotlin’s version.
The ruby version is similar to kotlin, but without the type safety:
class ConfigManager
def initialize(user_config, app_config, default_config)
@user_config = user_config
@app_config = app_config
@default_config = default_config
end
def timeout
@user_config&.timeout
|| @app_config&.timeout
|| @default_config&.timeout
|| 30_000
end
def database_url
ENV['DB_URL']
|| @user_config&.db_url
|| @app_config&.db_url
|| 'sqlite://memory'
end
endNamed parameter & default values
- https://kotlinlang.org/docs/functions.html#named-arguments
- https://kotlinlang.org/docs/functions.html#parameters-with-default-values
Named parameters and default values make function calls more readable and flexible. You can specify parameter names when calling functions, making the code self-documenting, and provide default values to avoid method overloading.
fun createUser(
name: String,
email: String,
age: Int = 0,
isActive: Boolean = true,
role: String = "user"
) {
// ...
}
// Usage with named parameters (order doesn't matter)
createUser(email = "john@example.com", name = "John Doe", age = 25)
// Mix of positional and named parameters
createUser("Jane Doe", "jane@example.com", role = "admin")
// Only required parameters
createUser("Bob", "bob@example.com")This eliminates the need for multiple constructor/method overloads and makes function calls much clearer, especially when dealing with boolean parameters or multiple optional parameters.
transferMoney(
from = sourceAccount,
to = destinationAccount,
amount = 1000.0,
includeFees = true,
async = false
)In java, you’d need multiple method overloads or use builder patterns:
// Java: multiple overloads needed
public void transferMoney(Account from, Account to, double amount) {
transferMoney(from, to, amount, true, false);
}
public void transferMoney(Account from, Account to, double amount, boolean includeFees) {
transferMoney(from, to, amount, includeFees, false);
}
public void transferMoney(Account from, Account to, double amount, boolean includeFees, boolean async) {
// ...
}
// Usage is unclear without IDE help
transferMoney(sourceAccount, destinationAccount, 1000.0, true, false);
// Or using builder pattern (more verbose)
TransferRequest.builder()
.from(sourceAccount)
.to(destinationAccount)
.amount(1000.0)
.includeFees(true)
.async(false)
.build()
.execute();The ruby version supports keyword arguments similarly to kotlin:
def create_user(name:, email:, age: 0, active: true, role: 'user')
# ...
end
# Usage with keyword arguments
create_user(email: 'john@example.com', name: 'John Doe', age: 25)
# Required parameters only
create_user(name: 'Bob', email: 'bob@example.com')However, ruby lacks compile-time type checking, so you might discover parameter type mismatches at runtime, while kotlin catches these at compile time.
Equality
https://kotlinlang.org/docs/equality.html
Kotlin distinguishes between structural equality (==) and referential equality (===).
==checks if two objects have the same value (callsequals()under the hood).===checks if two references point to the same object (identity).
val a = "hello"
val b = "hello"
val c = a
println(a == b) // true (structural equality)
println(a === b) // false (not necessarily the same reference)
println(a === c) // true (same reference)For data classes, == compares all properties by value automatically:
data class User(val name: String, val age: Int)
val u1 = User("Alice", 30)
val u2 = User("Alice", 30)
println(u1 == u2) // trueIn java, you must override equals() and hashCode() for value comparison, and it’s easy to make mistakes (e.g., using == for objects compares references, not values). Kotlin’s approach is safer and less error-prone.
it: implicit name of a single parameter
https://kotlinlang.org/docs/lambdas.html#it-implicit-name-of-a-single-parameter
It’s very common for a lambda expression to have only one parameter.
If the compiler can parse the signature without any parameters, the parameter does not need to be declared and -> can be omitted and the parameter will be implicitly declared under the name it:
val numbers = listOf(1, 2, 3, 4)
val doubled = numbers.map { it * 2 } // 'it' refers to each element
println(doubled) // [2, 4, 6, 8]
val firstEven = numbers.find { it % 2 == 0 }
println(firstEven) // 2In java, lambda parameters must always be named explicitly:
var numbers = List.of(1, 2, 3, 4);
var doubled = numbers.stream().map(n -> n * 2).toList();
System.out.println(doubled);
var firstEven = numbers.stream()
.filter(n -> n % 2 == 0)
.findFirst();
System.out.println(firstEven);Since ruby 3.4, it also exists:
numbers = [1, 2, 3, 4]
doubled = numbers.map { it * 2 }
puts doubled.inspect # [2, 4, 6, 8]
first_even = numbers.find { it.even? }
puts first_even # 2Data class copy
https://kotlinlang.org/docs/data-classes.html#copying
data class User(val name: String, val age: UInt) {}
val alice = User(name = "Alice", age = 24u)
val olderAlice = alice.copy(age = 48u)java does not have native feature for copying and updating immutable objects. Instead, we typically rely on design patterns such as the builder pattern or use third-party libraries like Lombok to achieve similar functionality:
@Builder(toBuilder = true)
public record User(String name, String age) {}
var alice = new User("Alice", 24);
var olderAlice = alice.toBuilder().age(48).build();In ruby, you can use the native Struct and dup to copy and update fields:
User = Struct.new(:name, :age)
alice = User.new(name: "Alice", age: 24)
older_alice = alice.dup.tap { it.age = 48 }String template
https://kotlinlang.org/docs/strings.html#string-templates
Kotlin supports string templates, allowing you to embed variables and expressions directly in strings using the $ symbol:
val name = "Alice"
val age = 24
println("Hello, $name! You are $age years old.")
println("Next year, you will be ${age + 1}.")In Java, string interpolation is not supported. You must use concatenation or String.format:
String name = "Alice";
int age = 24;
System.out.println("Hello, " + name + "! You are " + age + " years old.");
System.out.println(String.format("Next year, you will be %d.", age + 1));In Ruby, string interpolation uses #{} inside double-quoted strings:
name = "Alice"
age = 24
puts "Hello, #{name}! You are #{age} years old."
puts "Next year, you will be #{age + 1}."Operator overloading
https://kotlinlang.org/docs/operator-overloading.html#unary-operations
kotlin lets the possibility to override predefined symbolic representation (like + or *).
This led to a more readable code:
if (amount < BigDecimal.ZERO)instead of in java:
if (amount.compareTo(BigDecimal.ZERO) < 0)Another aspect of the operator overloading is the invoke operator which allows objects to be called as functions.
By implementing the operator fun invoke() function, you can make instances of a class callable with parentheses syntax, enabling function-like behavior for objects.
enum class SubscriptionDomainErrors(private val messageTemplate: String) {
INVALID_ACCOUNT_ID("Account id '%s' must be a positive"),
// ...
;
operator fun invoke(vararg args: Any): DomainError = SubscriptionDomainError(String.format(messageTemplate, *args), name)
}
// then it's called directly with parentheses:
if (value <= 0) throw DomainException(INVALID_ACCOUNT_ID(value))Syntactic sugar
let
https://kotlinlang.org/docs/scope-functions.html#let
Execute if not null:
val user = User(name = "Alice", age = 24)
val doubleAge = user?.let { it.age * 2 }Java version (using Optional):
User user = new User("Alice", 24);
Integer doubleAge = Optional.ofNullable(user).map(u -> u.getAge() * 2).orElse(null);Ruby version:
user = User.new(name: "Alice", age: 24)
double_age = user&.age&.*(2)collection.firstOrNull
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.collections/first-or-null.html
Returns the first element matching the given predicate, or null if no such element is found.
val numbers = listOf(1, 2, 3)
val firstEven = numbers.firstOrNull { it % 2 == 0 } // 2
val firstNegative = numbers.firstOrNull { it < 0 } // nullThe java version is more verbose:
List<Integer> numbers = Arrays.asList(1, 2, 3);
Integer firstEven = numbers.stream()
.filter(n -> n % 2 == 0)
.findFirst()
.orElse(null);
Integer firstNegative = numbers.stream()
.filter(n -> n < 0)
.findFirst()
.orElse(null);The ruby version is similar to kotlin:
numbers = [1, 2, 3]
first_even = numbers.find { it.even? }
first_negative = numbers.find { it < 0 }collection.associateBy
https://kotlinlang.org/docs/collection-transformations.html#associate
Association transformations allow building maps from the collection elements and certain values associated with them. In different association types, the elements can be either keys or values in the association map.
data class User(val id: Int, val name: String)
val users = listOf(User(1, "Alice"), User(2, "Bob"))
val userMap = users.associateBy { it.id }
// userMap: {1=User(id=1, name=Alice), 2=User(id=2, name=Bob)}The java version:
record User(int id, String name) {}
var users = List.of(new User(1, "Alice"), new User(2, "Bob"));
var userMap = users.stream().collect(Collectors.toMap(User::id, Function.identity()));The ruby version:
User = Struct.new(:id, :name)
users = [User.new(1, "Alice"), User.new(2, "Bob")]
user_map = users.to_h { |u| [u.id, u] }
# user_map: {1=>#<struct User id=1, name="Alice">, 2=>#<struct User id=2, name="Bob">}These methods make collection manipulation concise and expressive in kotlin, compared to more verbose approaches in Java.
Single-expression function
https://kotlinlang.org/docs/idioms.html#single-expression-functions
fun displayName(user: User?): String {
return user?.name ?: "Guest"
}
// can be more concise
fun displayName(user: User?): String = user?.name ?: "Guest"It can also be used with other idioms:
fun currentEndUser(): EndUser = when (val result = getCurrentEndUserResult()) {
is AuthResult.Success -> result.user
is AuthResult.Error -> throw AccessDeniedException(result.message)
}takeIf
https://kotlinlang.org/docs/scope-functions.html#takeif-and-takeunless
When called on an object along with a predicate, takeIf returns this object if it satisfies the given predicate.
Otherwise, it returns null. So, takeIf is a filtering function for a single object.
fun extractUserId(): Long? {
return tokenAttributes[ATTRIBUTE_USERID]
?.toString()
?.toLongOrNull()
?.takeIf { it > 0 }
}The java version is more verbose:
Long extractUserId() {
var attributes = getTokenAttributes().get(ATTRIBUTE_USERID);
if (attributes == null) return null
try {
var userId = Long.parseLong(v.toString());
return userId > 0 ? userId : null;
} catch (NumberFormatException ignored) {
return null;
}
}The ruby version is similar to kotlin:
def extract_user_id
token_attributes[ATTRIBUTE_USERID]
&.to_s
&.to_i
&.then { it > 0 ? it : nil }
endalso
https://kotlinlang.org/docs/scope-functions.html#also
also is useful for performing some actions that take the context object as an argument.
You can read it as ” and also do the following with the object. ”
// subscriptions: List<Subscription>
// providerSubscriptionIds: List<ProviderSubscriptionId>
val missingSubscriptions = subscriptions
.filter { it.providerSubscriptionId !in providerSubscriptionIds }
.map {
it.unsubscribeAsDeleted().also { s ->
logger.warn("Subscription ${s.id} unsubscribed as deleted as the provider did not contain this subscription.")
}
}In java:
var missingSubscriptions = subscriptions.stream()
.filter(not(sub -> providerSubscriptionIds.contains(sub.getProviderSubscriptionId())))
.map(sub -> {
Subscription s = sub.unsubscribeAsDeleted();
logger.warn("Subscription " + s.getId() + " unsubscribed as deleted as the provider did not contain this subscription.");
return s;
})
.toList();And the ruby version:
missing_subscriptions = subscriptions
.reject { |sub| provider_subscription_ids.include?(sub.provider_subscription_id) }
.map do |sub|
s = sub.unsubscribe_as_deleted
logger.warn("Subscription #{s.id} unsubscribed as deleted as the provider did not contain this subscription.")
s
endwith
https://kotlinlang.org/docs/scope-functions.html#with
with can be useful for calling functions on the context objects when you don’t need to use the returned result.
with can be read as “with this object, do the following.”
with(reloadedRevenueCatWebhook(webhookAppUserId)) {
assertThat(appUserId).isEqualTo(webhookAppUserId)
assertThat(body).isEqualTo(jacksonObjectMapper().readTree(payload))
assertThat(createdAt).isNotNull
}
// instead of
val webhook = reloadedRevenueCatWebhook(webhookAppUserId)
assertThat(webhook.appUserId).isEqualTo(webhookAppUserId)
assertThat(webhook.body).isEqualTo(jacksonObjectMapper().readTree(payload))
assertThat(webhook.createdAt).isNotNullbacktick method name
https://kotlinlang.org/docs/coding-conventions.html#names-for-test-methods
In kotlin, it’s possible to define method name with spaces using backticks:
fun `correct arguments`(description: String, executable: Executable) { /** **/ }This allows to have expressive test method names.
Extension function
https://kotlinlang.org/docs/idioms.html#extension-functions
This lets you add new functions to existing classes without modifying their source code.
// Extending the JwtAuthenticationToken class with a new method "extractUserId":
private fun JwtAuthenticationToken.extractUserId(): Long? =
tokenAttributes[ATTRIBUTE_USERID]
?.toString()
?.toLongOrNull()
?.takeIf { it > 0 }In java, we would have to create a helper function.
In ruby, we can also extend new methods:
class JwtAuthenticationToken
def extract_user_id
token_attributes[ATTRIBUTE_USERID]
&.to_s
&.to_i
&.then { it > 0 ? it : nil }
end
endThe unexpected
Build
I expected a slower build time, but I was pleasantly surprised that there were almost no build time overhead.
Multi-line string in parameterized tests
We are using JUnit Jupiter ParameterizedTest whenever we can, to test with multiple different values.
For example, we can have the following test:
class AccountIdTest {
@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, delimiterString = "->", textBlock = """
id -> expected error message
0 -> Account id '0' must be a positive
-1 -> Account id '-1' must be a positive
-42 -> Account id '-42' must be a positive
""")
fun `invalid arguments`(id: Long, expectedErrorMessage: String) {
// ..
}
}The test will fails because of:
org.junit.jupiter.api.extension.ParameterResolutionException: Error converting parameter at index 0: Cannot convert null to primitive value of type long
The issue here is that:
- the text block must be a constant (so not possible to use the
String.trimIndentmethod) - the test method is indented
- with an IDE, when pressing
Enter, the cursor will not be at the first column of the row, but at some indented columns
// The cursor | is placed with some indentation by default (the number of indentation depends on the IDE).
@CsvSource(useHeadersInDisplayName = true, delimiterString = "->", textBlock = """
|So the last line of:
@CsvSource(useHeadersInDisplayName = true, delimiterString = "->", textBlock = """
id -> expected error message
0 -> Account id '0' must be a positive
-1 -> Account id '-1' must be a positive
-42 -> Account id '-42' must be a positive
""")is also interpreted to be used in the test, so it’s trying to inject a null value, hence the error.
In java, there’s no problem, as it seems to skip this last line.
So the mitigation is either removing the last line:
@CsvSource(useHeadersInDisplayName = true, delimiterString = "->", textBlock = """
id -> expected error message
0 -> Account id '0' must be a positive
-1 -> Account id '-1' must be a positive
-42 -> Account id '-42' must be a positive""")or remove the margin:
@CsvSource(useHeadersInDisplayName = true, delimiterString = "->", textBlock = """
id -> expected error message
0 -> Account id '0' must be a positive
-1 -> Account id '-1' must be a positive
-42 -> Account id '-42' must be a positive
""")Superset primitives
kotlin has defined some of their own primitives like:
kotlin.time.Durationkotlin.collections.Listkotlin.collections.Mapkotlin.Function- …
It can be confusing to choose which class to use (java or kotlin, although default to kotlin might be a good choice as they offer more features).
The bad
LSP
There are some LSP out there:
- kotlin_language_server: community LSP server, not maintained
- kotlin_lsp: official LSP server implementation
Unfortunately, both are not usable for large multi-modules maven projects…
So I could not use my favorite editor nvim to code in kotlin efficiently. So go back to Jetbrain product, which I already used before, and their ideavim plugin is great.
Static methods and companion object
https://kotlinlang.org/docs/object-declarations.html#companion-objects
Companion objects allow you to define class-level functions and properties. This makes it easy to create factory methods, hold constants, and access shared utilities.
An object declaration inside a class can be marked with the companion keyword:
class MyClass {
companion object {
fun create(): MyClass = MyClass()
fun greet(name: String): String = "Hello, $name"
}
}
val a = MyClass.create()
print(MyClass.greet("Alice"))In ruby, it’s equivalent of self:
class MyClass
class << self
def create
new
end
def greet(name)
"Hello, #{name}"
end
end
end
var a = MyClass.create
puts MyClass.greet("Alice")The issue is that when using JUnit Jupiter parameterized tests, the @MethodSource which allows us to refer to one or more factory methods of the test class or external classes.
But it requires the factory method to be static.
In java:
@ParameterizedTest
@MethodSource("invalidArgumentsProvider")
void incorrectArguments(PatientId patientId, AccountId accountId, String expectedErrorMessage) {
// ...
}
private static Stream<Arguments> invalidArgumentsProvider() {
return Stream.of(
Arguments.of(null, new AccountId(123), "patient id is mandatory"),
Arguments.of(new PatientId("patient1"), null, "account id is mandatory")
);
}The equivalent in kotlin would be:
@ParameterizedTest
@MethodSource("invalidArgumentsProvider")
fun incorrectArguments(patientId: PatientId, accountId: AccountId, expectedErrorMessage: String) {
// ...
}
companion object {
@JvmStatic
fun invalidArgumentsProvider(): String<Arguments> = Stream.of(
Arguments.of(null, new AccountId(123), "patient id is mandatory"),
Arguments.of(new PatientId("patient1"), null, "account id is mandatory")
);
}The issue comes where we have multiple ParameterizedTest in the same test file.
As we can only have one companion object per class, all the static factory methods must be grouped together, whereas in java, we can co-locate the static factory method besides the test, which makes the test more readable, and have less code jumping.
Things I want to experiment
On Kotlin DSL
https://kotlinlang.org/docs/api-guidelines-readability.html#use-dsls
It’s possible to create DSL to improve readability in kotlin.
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
class Html {
private val children = mutableListOf<String>()
fun body(block: Body.() -> Unit) {
val body = Body().apply(block)
children.add(body.render())
}
fun render() = "<html>\n${children.joinToString("\n")}\n</html>"
}
@HtmlTagMarker
class Body {
private val children = mutableListOf<String>()
fun p(text: String) {
children.add("<p>$text</p>")
}
fun render() = "<body>\n${children.joinToString("\n")}\n</body>"
}
// Usage:
val html = Html().apply {
body {
p("Welcome to Kotlin DSLs!")
p("They are powerful and readable.")
}
}.render()
println(html)It might be more interesting for libraries instead of apps.
Asynchronous programming
https://kotlinlang.org/docs/async-programming.html
Troubleshooting
If you are getting the following issue where a global constant or a function are failing intellij compilation:
Kotlin: Overload resolution ambiguity between candidates:
val FOOBAR: Int
val FOOBAR: Int
It seems to be an issue with Kotlin incremental compilation. So I had to deactivate it with “Settings > Build, Execution, Deployment > Compiler > Kotlin comppiler” then uncheck “Enable incremental compilation”.
- side do the kotlin koan tutorial