Recap: Kotlin Dev Day 2025
The day before yesterday, I had the opportunity to attend Kotlin Dev Day. The event featured five parallel lanes, so my experience reflects just one perspective, yours could be completely different. Join me as I share the highlights of my day!
After a stressful drive into Dutch traffic, I finally made it to Circa Amsterdam just in time for the event. The program looked promising, so I was excited to see what the day would bring. After the usual registration process, I met my colleague Riccardo in the foyer, and together we headed to the first talk.
The opening keynote, presented by Michail Zarečenskij from JetBrains, focused on upcoming language features in Kotlin. Two things really stood out to me: the unused return value checker and name-based destructuring. The former will throw warnings when you perform actions that should have either be assigned to a value or returned:
val numbers = listOf(3, 1, 2)
// Developer intended to sort the list, but forgot to use the result
numbers.sorted() // <- Warning: Unused return value of 'sorted()'
println(numbers) // Still prints [3, 1, 2]
where the later changes position-based destructuring to using it by name:
data class User(val name: String, val id: Int)
// Before: Whoops: Variable name 'id' matches the name of a different component
val (id, name) = User(1, "Alice")
// After: for name-based destructuring the order does not matter
(val id, val name) = User(1, "Alice")
Then I attended a presentation by Jerre van Veluw, where he explained how to refactor a Spring Boot application by extracting the domain into a separate module. By leveraging interfaces and type classes, he not only decouples the domain but also eliminates most of Spring’s annotation-based dependency injection. Impressive work, especially for someone who loves functional programming like me!
Next came the most technically advanced session of the day for me. Elmar Wachtmeester demonstrated how to write your own JSON parser using only Kotlin functions. While the details are far too complex to fully explain here, conceptually it’s about composing simple functions on top of each other:
sealed class JsonValue {
data class JsonKey(val value: String) : JsonValue()
object Invalid : JsonValue()
}
fun isQuote(c: Char) = c == '"'
fun parseText(s: String) = if (s.isNotEmpty() && !s.contains('"')) s else null
fun parseJsonKey(input: String): JsonValue {
if (input.length < 2) return JsonValue.Invalid
return if (isQuote(input.first()) && isQuote(input.last())) {
parseText(input.substring(1, input.length - 1))?.let { JsonValue.JsonKey(it) } ?: JsonValue.Invalid
} else JsonValue.Invalid
}
println(parseJsonKey("\"hello\"")) // JsonKey(value=hello)
rintln(parseJsonKey("hello")) // Invalid
Just before lunch, Willem Veelenturf showed us how he built a Kotlin compiler plugin to rewrite mappers on the fly. He started by vibe-coding the plugin with Junie, and while AI proved helpful for boilerplate and providing insights about FIR and IR, it couldn’t generate the actual mapper logic. This meant Willem had to dive deep into how the Kotlin compiler produces the AST across multiple stages. Though he didn’t showcase much of the AST rewriting process, which I would have loved to see as someone interested in this kind of programming, I still consider this an awesome session!
After the session wrapped up, I headed to lunch, which was great! I don’t think I’ve ever been asked so many times if I wanted another bun 🤣.
With my belly full, I attended the “MCP in Action” session by Urs Peter. I don’t have much experience with MCPs; honestly, I only actually knew it does enrich a running LLM with extra options. It turns out the concept is quite simple: MCP is just a protocol that allows LLMs to communicate with other running processes. By leveraging this technique, you can enable your LLM to perform specific tasks. For example, searching within your Spring Boot application;
@RestController
class AiController(val mcpClient: McpClient) {
@GetMapping("/ask")
fun ask(@RequestParam q: String) =
"Results: ${mcpClient.callTool("searchLocalDb", mapOf("term" to q)).result}"
}
@Component
class DbSearchTool(val template: NamedParameterJdbcTemplate) {
@McpTool(name = "searchLocalDb", description = "Search local SQLite DB for a term")
fun search(term: String): List<String> {
val sql = "SELECT name FROM items WHERE name LIKE :term"
val params = mapOf("term" to "%$term%")
return template.queryForList(sql, params).map { it["name"].toString() }
}
}
Cool, so now I’ve got MCP protocol in my toolbelt as well! It’s time for some functional programming. In Kotlin, writing FP code often goes hand-in-hand with the Arrow library. Mehmet Akif Tütüncü demonstrated how easy this library is to use, focusing mainly on the Either and Option types. Even by using just these two constructs (the library offers much more), he showed how they improve code clarity. By modeling the error path as data, your logic becomes explicit and predictable:
fun divide(a: Int, b: Int): Either<String, Int> = either {
// If b is zero, raise an error -> this becomes the Left value of Either
if (b == 0) raise("Division by zero")
// If no error, return the result -> this becomes the Right value of Either
a / b
}
For the last session, I chose to learn more about Ktlint, the de facto formatter for Kotlin. I have been using this formatter for several years now, so I thought it would be nice to hear more about the behind-the-scenes of how it’s built and maintained. And Paul Dingemans delivered! As one of the main contributors and maintainers of the codebase, he clearly knew his way around the entire toolset. He explained how you can use the CLI, the Maven and Gradle plugins, and the IntelliJ plugin, and how they all fit into a typical Kotlin workflow. It got really interesting when he started showing how to write custom rules. At its core, the idea was surprisingly straightforward: you target a specific part of the Kotlin AST and forbid its usage when it matches your rule. Turns out it was not that hard at all if you are a little familiar with AST-targeted code:
class NoFooRule : Rule("no-foo") {
override fun visit(node: ASTNode, emit: (LintError) -> Unit) {
if (node.elementType == IDENTIFIER && node.text == "foo") {
emit(LintError(node.startOffset, node.lineAndColumn().second, "Using 'foo' is not allowed"))
}
}
}
To close the day together, there was one more roundtable session. A group of developers and developer advocates from JetBrains were there to answer anything Kotlin related. By using the power of Kahoot!, we were able to steer the conversation. Lots of interesting things were discussed, ranging from practical tooling questions to long-standing feature debates. For me, it was especially nice to know I am not the only one who still has high hopes of true union types (come on JetBrains, it’s been 9 years 😜).
After the session wrapped up, a proper Dutch borrel was already in full swing on the second floor. I grabbed a beer, caught up with some former and current colleagues, and inevitably ended up with a bitterbal or two. Looking back, it was a great day full of knowledge, sharp conversations and the kind of nerd enthusiasm I really like. Until next time!