Skip to content

Advanced Kotlin

This is where Kotlin stops looking like “a nicer Java” and starts showing what makes it uniquely expressive: type-safe builders, first-class delegation, inline/reified generics that beat JVM type erasure, context parameters, and compile-time code generation with KSP. These are the features you reach for when you want libraries that read like configuration and abstractions that cost nothing at runtime — things you can only fake in TypeScript and Go.

A Domain-Specific Language (DSL) is a mini-language tailored to a problem domain. Kotlin’s combination of lambda-with-receiver, extension functions, and trailing lambda syntax makes it the best mainstream language for internal DSLs.

Three Kotlin features combine to enable DSLs: extension functions add methods to existing types, a lambda with receiver makes this refer to a receiver object, and trailing lambda syntax moves the last lambda param outside the parentheses.

// 1. Extension functions — add methods to existing types
fun String.shout() = this.uppercase() + "!!!"
"hello".shout() // "HELLO!!!"
// 2. Lambda with receiver — a lambda where `this` refers to a receiver object
fun buildString(block: StringBuilder.() -> Unit): String {
val sb = StringBuilder()
sb.block() // invoke the lambda with sb as receiver
return sb.toString()
}
val result = buildString {
append("Hello, ") // `this` is StringBuilder
append("World!")
}
// result = "Hello, World!"
// 3. Trailing lambda syntax — last lambda param goes outside parentheses
// Combined with the above, this gives us "block" syntax that looks like new keywords

The key trick is the receiver type on the lambda parameter — StringBuilder.() -> Unit rather than () -> Unit. That single .() is what lets the block body call StringBuilder methods as if this were implicit.

Cross-language comparison: builder patterns

Section titled “Cross-language comparison: builder patterns”

How TS, Go, and Kotlin each approach “configure an object with a block”:

// TS has no native DSL support. You fake it with method chaining:
class QueryBuilder {
private parts: string[] = [];
select(columns: string): this {
this.parts.push(`SELECT ${columns}`);
return this;
}
from(table: string): this {
this.parts.push(`FROM ${table}`);
return this;
}
where(condition: string): this {
this.parts.push(`WHERE ${condition}`);
return this;
}
build(): string {
return this.parts.join(" ");
}
}
const query = new QueryBuilder()
.select("name, email")
.from("users")
.where("active = true")
.build();

Key differences:

  • TypeScript relies on method chaining (fluent builders) — no nesting, no compile-time scope control.
  • Go uses functional options — type-safe but no nesting or block syntax.
  • Kotlin DSLs support arbitrarily deep nesting, compile-time type checking at every level, and IDE autocompletion inside every block.

A self-contained DSL: each tag class extends a base Tag, and nesting methods like head { ... } and body { ... } take a Head.() -> Unit / Body.() -> Unit receiver lambda.

// Step 1: Define the domain model
@DslMarker
annotation class HtmlDsl
@HtmlDsl
open class Tag(var name: String) {
val children = mutableListOf<Tag>()
val attributes = mutableMapOf<String, String>()
protected var textContent: String = ""
fun attribute(key: String, value: String) {
attributes[key] = value
}
override fun toString(): String {
val attrStr = if (attributes.isEmpty()) "" else
attributes.entries.joinToString(" ", prefix = " ") { "${it.key}=\"${it.value}\"" }
val childStr = children.joinToString("\n") { " $it" }
val content = if (textContent.isNotEmpty()) textContent else childStr
return "<$name$attrStr>\n$content\n</$name>"
}
}
// Step 2: Create specific tag classes
@HtmlDsl
class Html : Tag("html") {
fun head(block: Head.() -> Unit) {
val head = Head()
head.block()
children.add(head)
}
fun body(block: Body.() -> Unit) {
val body = Body()
body.block()
children.add(body)
}
}
@HtmlDsl
class Head : Tag("head") {
fun title(text: String) {
val t = Tag("title")
t.textContent = text // direct access within same module
children.add(t)
}
fun meta(charset: String) {
val m = Tag("meta")
m.attribute("charset", charset)
children.add(m)
}
}
@HtmlDsl
class Body : Tag("body") {
fun h1(text: String) {
val h = Tag("h1")
h.textContent = text
children.add(h)
}
fun p(text: String) {
val p = Tag("p")
p.textContent = text
children.add(p)
}
fun div(block: Body.() -> Unit) {
val d = Body() // reuse Body for nested blocks
d.name = "div" // would need to be mutable — see note
d.block()
children.add(d)
}
fun a(href: String, text: String) {
val a = Tag("a")
a.attribute("href", href)
a.textContent = text
children.add(a)
}
operator fun String.unaryPlus() {
val t = Tag("span")
t.textContent = this
children.add(t)
}
}
// Step 3: Top-level DSL entry point
fun html(block: Html.() -> Unit): Html {
val html = Html()
html.block()
return html
}
// Step 4: Use it!
fun main() {
val page = html {
head {
meta("UTF-8")
title("My Page")
}
body {
h1("Welcome")
p("This is a type-safe HTML builder.")
a("https://kotlinlang.org", "Kotlin")
}
}
println(page)
}

Without @DslMarker, inner lambdas can access outer receivers, leading to bugs. Inside head { ... }, the Html.body() method is still in implicit scope, so body { } compiles even though it makes no sense inside <head>:

// WITHOUT @DslMarker — dangerous scope leakage
html {
head {
// BUG: `body {}` is accessible here because Html's body() is in scope!
body { } // compiles but makes no sense inside <head>
}
}
// WITH @DslMarker — compiler error
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Html { /* ... */ }
@HtmlDsl
class Head { /* ... */ }
html {
head {
body { } // ERROR: 'fun body(...)' can't be called in this context
// by implicit receiver. Use the explicit receiver if needed.
}
}

How @DslMarker works:

  1. You create an annotation marked with @DslMarker.
  2. You apply it to all DSL classes.
  3. The compiler restricts implicit access to only the nearest receiver.
  4. To access an outer receiver, you must use a labeled reference: this@html.body { }.
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class Html { /* ... */ }
@HtmlDsl
class Head { /* ... */ }
@HtmlDsl
class Body { /* ... */ }
html {
// Inside here, `this` is Html
head {
// Inside here, `this` is Head
// Html's methods are NOT implicitly available
// To explicitly call Html methods:
this@html.body { } // OK — explicit outer receiver
}
}

Kotlin’s standard library and ecosystem are full of DSLs — and you’ve already been using several of them in this course (your Gradle build files are DSLs).

// 1. kotlinx.html — production HTML DSL
import kotlinx.html.*
import kotlinx.html.stream.createHTML
val html = createHTML().html {
body {
div {
classes = setOf("container")
h1 { +"Welcome" }
p { +"Type-safe HTML" }
}
}
}
// 2. Ktor routing — the DSL you'll replicate in the exercise
routing {
route("/api/v1") {
get("/users") {
call.respond(userService.findAll())
}
post("/users") {
val user = call.receive<CreateUserRequest>()
call.respond(HttpStatusCode.Created, userService.create(user))
}
}
}
// 3. Gradle Kotlin DSL — your build files are DSLs!
plugins {
kotlin("jvm") version "2.0.0"
application
}
dependencies {
implementation("io.ktor:ktor-server-core:3.0.0")
testImplementation(kotlin("test"))
}
// 4. Exposed SQL DSL
object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 255)
val email = varchar("email", 255)
override val primaryKey = PrimaryKey(id)
}
transaction {
Users.select { Users.name like "%kotlin%" }
.forEach { println(it[Users.email]) }
}
// 5. Kotest — test DSL
class UserServiceTest : StringSpec({
"should create user" {
val user = userService.create("Alice")
user.name shouldBe "Alice"
}
"should reject empty name" {
shouldThrow<IllegalArgumentException> {
userService.create("")
}
}
})

A practical example — a type-safe application configuration DSL. Each nested block (server { }, cors { }, database { }) creates a config object and applies the block to it with apply, so the whole thing reads like YAML but is type-checked.

@DslMarker
annotation class ConfigDsl
@ConfigDsl
class AppConfig {
var name: String = ""
var version: String = "1.0.0"
private var _server: ServerConfig? = null
private var _database: DatabaseConfig? = null
private var _logging: LoggingConfig? = null
val server: ServerConfig get() = _server ?: error("Server not configured")
val database: DatabaseConfig get() = _database ?: error("Database not configured")
val logging: LoggingConfig get() = _logging ?: error("Logging not configured")
fun server(block: ServerConfig.() -> Unit) {
_server = ServerConfig().apply(block)
}
fun database(block: DatabaseConfig.() -> Unit) {
_database = DatabaseConfig().apply(block)
}
fun logging(block: LoggingConfig.() -> Unit) {
_logging = LoggingConfig().apply(block)
}
}
@ConfigDsl
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
var ssl: Boolean = false
private var _cors: CorsConfig? = null
val cors: CorsConfig? get() = _cors
fun cors(block: CorsConfig.() -> Unit) {
_cors = CorsConfig().apply(block)
}
}
@ConfigDsl
class CorsConfig {
val allowedOrigins = mutableListOf<String>()
val allowedMethods = mutableListOf<String>()
fun origin(url: String) { allowedOrigins.add(url) }
fun method(m: String) { allowedMethods.add(m) }
}
@ConfigDsl
class DatabaseConfig {
var url: String = ""
var username: String = ""
var password: String = ""
var maxPoolSize: Int = 10
var minIdle: Int = 2
}
@ConfigDsl
class LoggingConfig {
var level: String = "INFO"
var format: String = "json"
var file: String? = null
}
// Top-level entry point
fun appConfig(block: AppConfig.() -> Unit): AppConfig =
AppConfig().apply(block)
// Usage — reads like YAML but is type-checked Kotlin
fun main() {
val config = appConfig {
name = "my-service"
version = "2.1.0"
server {
host = "0.0.0.0"
port = 9090
ssl = true
cors {
origin("https://example.com")
origin("https://app.example.com")
method("GET")
method("POST")
method("PUT")
}
}
database {
url = "jdbc:postgresql://localhost:5432/mydb"
username = "app_user"
password = System.getenv("DB_PASSWORD") ?: "dev-password"
maxPoolSize = 20
}
logging {
level = "DEBUG"
format = "json"
file = "/var/log/my-service.log"
}
}
println("Starting ${config.name} v${config.version}")
println("Server: ${config.server.host}:${config.server.port}")
println("Database: ${config.database.url}")
println("Log level: ${config.logging.level}")
}

Kotlin has first-class support for the delegation pattern — both for classes and properties. This eliminates the boilerplate that TS and Go developers write by hand.

The problem: you want to compose behavior from multiple interfaces without deep inheritance hierarchies.

interface Logger {
log(message: string): void;
error(message: string): void;
}
interface Metrics {
increment(counter: string): void;
gauge(name: string, value: number): void;
}
class ConsoleLogger implements Logger {
log(message: string) { console.log(message); }
error(message: string) { console.error(message); }
}
class PrometheusMetrics implements Metrics {
increment(counter: string) { /* ... */ }
gauge(name: string, value: number) { /* ... */ }
}
// Manual delegation — you must write every method
class Service implements Logger, Metrics {
constructor(
private logger: Logger = new ConsoleLogger(),
private metrics: Metrics = new PrometheusMetrics()
) {}
// Tedious: forward every method
log(message: string) { this.logger.log(message); }
error(message: string) { this.logger.error(message); }
increment(counter: string) { this.metrics.increment(counter); }
gauge(name: string, value: number) { this.metrics.gauge(name, value); }
}

Key differences:

  • TypeScript requires writing every forwarding method manually.
  • Go embedding auto-promotes methods but doesn’t implement interfaces explicitly.
  • Kotlin by is the best of both: zero boilerplate, explicit interface implementation, and you can override individual methods.

The by delegate clause forwards everything by default, so a decorator only needs to override the methods it wants to wrap.

interface HttpClient {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}
class RealHttpClient : HttpClient {
override suspend fun get(url: String): String { /* real HTTP */ return "" }
override suspend fun post(url: String, body: String): String { /* real HTTP */ return "" }
}
// Logging decorator — delegates everything, wraps specific methods
class LoggingHttpClient(
private val delegate: HttpClient
) : HttpClient by delegate {
override suspend fun get(url: String): String {
println("GET $url")
val result = delegate.get(url)
println("GET $url -> ${result.length} bytes")
return result
}
override suspend fun post(url: String, body: String): String {
println("POST $url (${body.length} bytes)")
val result = delegate.post(url, body)
println("POST $url -> ${result.length} bytes")
return result
}
}
// Retry decorator — wraps all methods
class RetryHttpClient(
private val delegate: HttpClient,
private val maxRetries: Int = 3
) : HttpClient by delegate {
override suspend fun get(url: String): String = retry { delegate.get(url) }
override suspend fun post(url: String, body: String): String = retry { delegate.post(url, body) }
private suspend fun <T> retry(block: suspend () -> T): T {
var lastException: Exception? = null
repeat(maxRetries) {
try { return block() }
catch (e: Exception) { lastException = e }
}
throw lastException!!
}
}
// Stack decorators
fun main() {
val client: HttpClient = LoggingHttpClient(
RetryHttpClient(
RealHttpClient(),
maxRetries = 3
)
)
}

Kotlin lets you delegate the getter and setter of a property to another object. The standard library provides several built-in delegates.

lazy initializes on first read and is thread-safe by default:

class ExpensiveService {
val connection: DatabaseConnection by lazy {
println("Creating connection...")
DatabaseConnection("jdbc:postgresql://localhost/mydb")
}
val config: Map<String, String> by lazy {
println("Loading config from disk...")
loadConfigFromFile("/etc/app.conf")
}
}
fun main() {
val svc = ExpensiveService()
println("Service created") // nothing loaded yet
println(svc.connection) // "Creating connection..." then value
println(svc.connection) // cached — no message
}

Thread-safety modes let you trade safety for speed:

// Default: synchronized (thread-safe, some overhead)
val a by lazy { compute() }
// Publication: multiple threads may compute, but all see same result
val b by lazy(LazyThreadSafetyMode.PUBLICATION) { compute() }
// None: no synchronization (fastest, use in single-threaded context)
val c by lazy(LazyThreadSafetyMode.NONE) { compute() }

The TypeScript equivalent is a memoized getter — same idea, much more boilerplate (and no thread-safety concern in single-threaded JS):

class ExpensiveService {
private _connection?: DatabaseConnection;
get connection(): DatabaseConnection {
if (!this._connection) {
this._connection = new DatabaseConnection("jdbc:...");
}
return this._connection;
}
}
import kotlin.properties.Delegates
class UserPreferences {
var theme: String by Delegates.observable("light") { prop, old, new ->
println("Theme changed: $old -> $new")
saveToStorage(prop.name, new)
}
var fontSize: Int by Delegates.observable(14) { _, old, new ->
println("Font size: $old -> $new")
}
}
fun main() {
val prefs = UserPreferences()
prefs.theme = "dark" // "Theme changed: light -> dark"
prefs.fontSize = 18 // "Font size: 14 -> 18"
}
import kotlin.properties.Delegates
class Account {
var balance: Double by Delegates.vetoable(0.0) { _, old, new ->
if (new < 0) {
println("REJECTED: balance cannot be negative (attempted: $new)")
false // reject the change
} else {
println("Balance: $old -> $new")
true // accept the change
}
}
}
fun main() {
val account = Account()
account.balance = 100.0 // "Balance: 0.0 -> 100.0"
account.balance = -50.0 // "REJECTED: balance cannot be negative"
println(account.balance) // 100.0 (unchanged)
}

Properties can be backed by a Map, where the keys match the property names — great for config or JSON-shaped data:

class User(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
val email: String by map
}
fun main() {
val userData = mapOf(
"name" to "Alice",
"age" to 30,
"email" to "alice@example.com"
)
val user = User(userData)
println("${user.name}, ${user.age}, ${user.email}")
// Alice, 30, alice@example.com
}
// Mutable version — assignments write back into the map
class MutableUser(map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
var email: String by map
}
fun mutableExample() {
val data = mutableMapOf<String, Any?>(
"name" to "Bob",
"age" to 25,
"email" to "bob@example.com"
)
val user = MutableUser(data)
user.name = "Robert"
println(data["name"]) // "Robert" — the map is updated!
}

You can create your own delegates by implementing ReadOnlyProperty or ReadWriteProperty. A delegate just needs getValue (and, for read-write, setValue) taking a thisRef and a KProperty<*>:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// Custom delegate: trim whitespace automatically
class TrimmedString(private var value: String = "") : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim()
}
}
// Helper function for cleaner syntax
fun trimmed(initial: String = "") = TrimmedString(initial)
class UserForm {
var name: String by trimmed()
var email: String by trimmed()
var bio: String by trimmed()
}
fun main() {
val form = UserForm()
form.name = " Alice "
form.email = " alice@example.com "
println("'${form.name}'") // 'Alice'
println("'${form.email}'") // 'alice@example.com'
}

A more practical example — an environment-variable delegate that reads from System.getenv, defaulting the variable name to the uppercased property name:

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class EnvironmentVariable(
private val name: String? = null,
private val default: String? = null,
private val required: Boolean = true
) : ReadOnlyProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
val envName = name ?: property.name.uppercase().replace('.', '_')
return System.getenv(envName)
?: default
?: if (required) error("Required environment variable '$envName' is not set")
else ""
}
}
fun env(name: String? = null, default: String? = null, required: Boolean = true) =
EnvironmentVariable(name, default, required)
// Usage — clean, declarative config
object AppConfig {
val databaseUrl: String by env("DATABASE_URL")
val databaseUser: String by env("DATABASE_USER", default = "postgres")
val databasePassword: String by env("DATABASE_PASSWORD")
val serverPort: String by env("SERVER_PORT", default = "8080")
val logLevel: String by env("LOG_LEVEL", default = "INFO")
val debugMode: String by env("DEBUG_MODE", default = "false", required = false)
}
fun main() {
println("Port: ${AppConfig.serverPort}")
println("Log level: ${AppConfig.logLevel}")
}

A cached/expiring-value delegate adds a TTL on top of a loader function:

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class CachedValue<T>(
private val ttlMillis: Long,
private val loader: () -> T
) : ReadOnlyProperty<Any?, T> {
private var cachedValue: T? = null
private var lastLoadTime: Long = 0
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
val now = System.currentTimeMillis()
if (cachedValue == null || (now - lastLoadTime) > ttlMillis) {
cachedValue = loader()
lastLoadTime = now
}
@Suppress("UNCHECKED_CAST")
return cachedValue as T
}
}
fun <T> cached(ttlMillis: Long, loader: () -> T) = CachedValue(ttlMillis, loader)
// Usage
class FeatureFlagService {
val flags: Map<String, Boolean> by cached(ttlMillis = 60_000) {
println("Loading feature flags from remote...")
mapOf("new-ui" to true, "beta-feature" to false)
}
}

Understanding the bytecode helps you reason about performance. Class delegation stores the delegate as a field and generates plain forwarding methods — no runtime reflection, no virtual dispatch overhead beyond the interface call:

// SOURCE:
class Service(logger: Logger) : Logger by logger
// WHAT THE COMPILER GENERATES (approximately):
class Service(private val delegate_logger: Logger) : Logger {
override fun log(message: String) = delegate_logger.log(message)
override fun error(message: String) = delegate_logger.error(message)
}

Property delegation desugars to a hidden $delegate field plus a getter that reads through it:

// SOURCE:
val name: String by lazy { "Alice" }
// WHAT THE COMPILER GENERATES (approximately):
private val nameDelegate = lazy { "Alice" }
val name: String get() = nameDelegate.value
// The Lazy<T> instance handles synchronization.

The problem with JVM generics (type erasure)

Section titled “The problem with JVM generics (type erasure)”

The JVM erases generic type parameters at runtime — inherited from Java. A bare value is T won’t compile because T is gone after compilation:

// This DOES NOT WORK:
fun <T> isInstanceOf(value: Any): Boolean {
return value is T // ERROR: Cannot check for instance of erased type: T
}
// Because after compilation, T is gone:
// fun isInstanceOf(value: Any): Boolean {
// return value is ??? // T is erased, JVM doesn't know what T was
// }

TypeScript and Go handle this differently:

// TypeScript has the SAME problem — types are erased at compile time
function isInstanceOf<T>(value: any): value is T {
// No way to check T at runtime — TS types don't exist at runtime
return false; // you'd need a runtime type guard
}

inline tells the compiler to copy the function body into every call site, eliminating the lambda object allocation:

// Without inline: a lambda object is created on the heap
fun measure(block: () -> Unit) {
val start = System.nanoTime()
block() // block is an object with invoke() method
val elapsed = System.nanoTime() - start
println("Elapsed: ${elapsed}ns")
}
// With inline: the lambda body is pasted directly into the call site
inline fun measureInline(block: () -> Unit) {
val start = System.nanoTime()
block() // replaced with the actual lambda body at compile time
val elapsed = System.nanoTime() - start
println("Elapsed: ${elapsed}ns")
}
fun main() {
// This:
measureInline {
Thread.sleep(100)
}
// Compiles to (roughly):
val start = System.nanoTime()
Thread.sleep(100) // inlined directly!
val elapsed = System.nanoTime() - start
println("Elapsed: ${elapsed}ns")
}

When a function is inline you can opt individual lambda params out with noinline (needed if you store the lambda) or restrict them with crossinline (forbids non-local returns):

inline fun execute(
block: () -> Unit, // will be inlined
noinline callback: () -> Unit // will NOT be inlined (kept as object)
) {
block()
// We need `noinline` here because we're storing the lambda:
val savedCallback = callback // can't store an inlined lambda
// use savedCallback later...
}
// crossinline: prevents non-local returns in the lambda
inline fun runInThread(crossinline block: () -> Unit) {
Thread {
block() // without crossinline, `return` here would try to return from
// the enclosing function, which is impossible from another thread
}.start()
}

Because inlined lambdas are pasted into the caller, a return inside them returns from the enclosing function — a non-local return, a unique Kotlin feature:

inline fun repeatUntil(condition: () -> Boolean, block: () -> Unit) {
while (true) {
block()
if (condition()) return // returns from repeatUntil
}
}
fun findFirst(items: List<String>, target: String): String? {
var result: String? = null
items.forEach { item -> // forEach is inline
if (item == target) {
result = item
return result // non-local return — exits findFirst!
}
}
return null
}

reified + inline means type parameters survive at runtime, because the type is substituted at each call site. Now value is T and T::class both work:

// Now this WORKS:
inline fun <reified T> isInstanceOf(value: Any): Boolean {
return value is T // OK! T is known at compile time at each call site
}
fun main() {
println(isInstanceOf<String>("hello")) // true
println(isInstanceOf<Int>("hello")) // false
println(isInstanceOf<String>(42)) // false
}

Practical uses of reified generics — type-safe JSON parsing, a tiny service locator, logger creation, and filtering by type:

// 1. Type-safe JSON deserialization
inline fun <reified T> String.parseJson(): T {
val mapper = jacksonObjectMapper()
return mapper.readValue(this, T::class.java)
// ^^^^^^^^^^^ only possible with reified!
}
val user: User = """{"name":"Alice","age":30}""".parseJson()
val items: List<Item> = """[{"id":1},{"id":2}]""".parseJson()
// 2. Service locator / simple DI
class ServiceLocator {
private val services = mutableMapOf<KClass<*>, Any>()
fun <T : Any> register(clazz: KClass<T>, instance: T) {
services[clazz] = instance
}
inline fun <reified T : Any> get(): T {
return services[T::class] as? T
?: error("No service registered for ${T::class.simpleName}")
}
}
val locator = ServiceLocator()
locator.register(UserService::class, UserServiceImpl())
// Look ma, no class parameter!
val userService = locator.get<UserService>()
// 3. Type-safe logger creation
inline fun <reified T> logger(): Logger {
return LoggerFactory.getLogger(T::class.java)
}
class UserController {
private val log = logger<UserController>()
}
// 4. Filtering collections by type — filterIsInstance uses reified generics
val mixed: List<Any> = listOf(1, "hello", 2.0, "world", 3)
val strings: List<String> = mixed.filterIsInstance<String>() // ["hello", "world"]
val ints: List<Int> = mixed.filterIsInstance<Int>() // [1, 3]

Contracts let you tell the compiler facts about your function’s behavior — for example that a true return implies the receiver is non-null, or that a lambda is called exactly once:

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && this.isNotEmpty()
}
fun processName(name: String?) {
if (name.isNotNullOrEmpty()) {
// Compiler knows name is non-null here because of the contract!
println(name.uppercase()) // no ?. needed
}
}
@OptIn(ExperimentalContracts::class)
inline fun <R> executeExactlyOnce(block: () -> R): R {
contract {
callsInPlace(block, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
}
return block()
}
fun demo() {
val x: Int
executeExactlyOnce {
x = 42 // OK — compiler knows block runs exactly once, so x is definitely initialized
}
println(x) // OK — x is definitely assigned
}

You have functions that need access to “ambient” services (logger, DB connection, transaction, coroutine scope) without passing them explicitly every time. The usual options each have a downside:

// Approach 1: Pass explicitly — verbose
fun createUser(
name: String,
db: Database,
logger: Logger,
metrics: Metrics,
validator: Validator
): User {
logger.log("Creating user $name")
metrics.increment("user.created")
validator.validate(name)
return db.insert(User(name = name))
}
// Approach 2: Global singletons — hard to test
fun createUser(name: String): User {
GlobalLogger.log("Creating user $name") // untestable
GlobalMetrics.increment("user.created") // untestable
return GlobalDb.insert(User(name = name)) // untestable
}
// Approach 3: DI framework (Spring) — magic annotations
@Service
class UserService(
private val db: Database, // injected
private val logger: Logger, // injected
private val metrics: Metrics // injected
) {
fun createUser(name: String): User { /* ... */ }
}
// TS devs use constructor injection or closures:
class UserService {
constructor(
private db: Database,
private logger: Logger,
private metrics: Metrics
) {}
createUser(name: string): User {
this.logger.log(`Creating user ${name}`);
this.metrics.increment("user.created");
return this.db.insert({ name });
}
}

The refined design uses named context(...) parameter syntax:

// Define interfaces for your contexts
interface LoggingContext {
fun log(message: String)
fun error(message: String)
}
interface TransactionContext {
fun <T> execute(block: () -> T): T
fun rollback()
}
// Functions declare what contexts they need
context(logCtx: LoggingContext)
fun processOrder(orderId: String) {
logCtx.log("Processing order $orderId")
// ...
}
context(logCtx: LoggingContext, txCtx: TransactionContext)
fun createOrder(items: List<Item>): Order {
logCtx.log("Creating order with ${items.size} items")
return txCtx.execute {
val order = Order(items = items)
// save to DB
order
}
}
// Extension functions with context
context(logCtx: LoggingContext)
fun Order.validate(): Boolean {
logCtx.log("Validating order ${this.id}")
return this.items.isNotEmpty()
}

Until context parameters stabilize: extension function pattern

Section titled “Until context parameters stabilize: extension function pattern”

A practical approach that works today without experimental flags: declare a ServiceContext interface holding the ambient services, then write your operations as extension functions on it and call them inside with(ctx) { ... }:

interface ServiceContext {
val logger: Logger
val db: Database
val metrics: Metrics
}
// Extension functions on ServiceContext
fun ServiceContext.createUser(name: String): User {
logger.info("Creating user $name")
metrics.increment("users.created")
return db.insert(User(name = name))
}
fun ServiceContext.findUser(id: Long): User? {
logger.info("Finding user $id")
return db.findById(id)
}
// Implementation
class ProductionContext(
override val logger: Logger,
override val db: Database,
override val metrics: Metrics
) : ServiceContext
// Usage
fun main() {
val ctx = ProductionContext(
logger = Slf4jLogger(),
db = PostgresDb(),
metrics = PrometheusMetrics()
)
with(ctx) {
val user = createUser("Alice")
val found = findUser(user.id)
}
}
// Testing — easy to mock
class TestContext : ServiceContext {
override val logger = NoOpLogger()
override val db = InMemoryDb()
override val metrics = NoOpMetrics()
}

KSP (Kotlin Symbol Processing) is Kotlin’s compile-time code generation tool. It reads your Kotlin source code as a symbol tree and generates new Kotlin/Java files. Compared to the other ecosystems:

FeatureTypeScriptGoKotlin
MechanismDecorators + reflect-metadatago generate + AST parsingKSP (compiler plugin)
Runs atRuntime (decorators)Before compile (go generate)During compilation
Type infoLimited runtime reflectionFull AST accessFull symbol + type info
OutputRuntime behavior modificationGenerated .go filesGenerated .kt/.java files
// TS decorators run at RUNTIME — they modify classes after compilation
function Entity(tableName: string) {
return function (constructor: Function) {
Reflect.defineMetadata("tableName", tableName, constructor);
};
}
function Column(options?: { primary?: boolean }) {
return function (target: any, propertyKey: string) {
const columns = Reflect.getMetadata("columns", target.constructor) || [];
columns.push({ name: propertyKey, ...options });
Reflect.defineMetadata("columns", columns, target.constructor);
};
}
@Entity("users")
class User {
@Column({ primary: true })
id!: number;
@Column()
name!: string;
}

KSP runs as a plugin inside the Kotlin compiler: it reads the symbol tree, hands it to your processor, which writes new .kt files that get compiled alongside the originals.

KSP code-generation pipeline
Rendering diagram…

A processor implements SymbolProcessor.process, finds annotated symbols via the Resolver, and writes files through the CodeGenerator. Here a processor reads every @AutoBuilder class and emits a matching builder:

// build.gradle.kts for the processor module
plugins {
kotlin("jvm")
}
dependencies {
implementation("com.google.devtools.ksp:symbol-processing-api:2.0.0-1.0.22")
}
// --- Annotation definition (shared module) ---
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class AutoBuilder
// --- Processor implementation ---
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
class AutoBuilderProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
// Find all classes annotated with @AutoBuilder
val annotated = resolver
.getSymbolsWithAnnotation("com.example.AutoBuilder")
.filterIsInstance<KSClassDeclaration>()
annotated.forEach { classDecl ->
generateBuilder(classDecl)
}
return emptyList() // nothing to defer
}
private fun generateBuilder(classDecl: KSClassDeclaration) {
val className = classDecl.simpleName.asString()
val packageName = classDecl.packageName.asString()
val builderName = "${className}Builder"
// Get constructor parameters
val params = classDecl.primaryConstructor?.parameters ?: return
// Generate builder class
val file = codeGenerator.createNewFile(
Dependencies(true, classDecl.containingFile!!),
packageName,
builderName
)
file.bufferedWriter().use { writer ->
writer.write("package $packageName\n\n")
writer.write("class $builderName {\n")
// Generate mutable properties
params.forEach { param ->
val name = param.name?.asString() ?: return@forEach
val type = param.type.resolve().declaration.qualifiedName?.asString() ?: "Any"
writer.write(" private var $name: $type? = null\n")
}
writer.write("\n")
// Generate setter methods
params.forEach { param ->
val name = param.name?.asString() ?: return@forEach
val type = param.type.resolve().declaration.qualifiedName?.asString() ?: "Any"
writer.write(" fun $name(value: $type): $builderName {\n")
writer.write(" this.$name = value\n")
writer.write(" return this\n")
writer.write(" }\n\n")
}
// Generate build method
writer.write(" fun build(): $className {\n")
writer.write(" return $className(\n")
params.forEachIndexed { index, param ->
val name = param.name?.asString() ?: return@forEachIndexed
val comma = if (index < params.size - 1) "," else ""
writer.write(" $name = $name ?: error(\"$name is required\")$comma\n")
}
writer.write(" )\n")
writer.write(" }\n")
writer.write("}\n\n")
// Generate extension function
writer.write("fun $className.Companion.builder(): $builderName = $builderName()\n")
}
logger.info("Generated builder for $className")
}
}
// --- Provider (registers the processor) ---
class AutoBuilderProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return AutoBuilderProcessor(
environment.codeGenerator,
environment.logger
)
}
}

Register the provider in a services file so KSP discovers it:

src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider
com.example.AutoBuilderProvider

Then the app module applies the KSP plugin, depends on the processor with the ksp configuration, and gets a generated builder for free:

// build.gradle.kts for the app module
plugins {
kotlin("jvm")
id("com.google.devtools.ksp") version "2.0.0-1.0.22"
}
dependencies {
implementation(project(":annotations")) // shared annotation module
ksp(project(":processor")) // the KSP processor module
}
// Application code
@AutoBuilder
data class User(
val name: String,
val email: String,
val age: Int
) {
companion object
}
// After compilation, you can use the generated builder:
fun main() {
val user = User.builder()
.name("Alice")
.email("alice@example.com")
.age(30)
.build()
}

Many frameworks use KSP (or KAPT, its predecessor) instead of runtime reflection:

FrameworkWhat it generates
Room (Android)SQL query implementations from DAO interfaces
Dagger/HiltDependency injection wiring
kotlinx.serializationJSON/Protobuf serializers from @Serializable
MoshiJSON adapter from @JsonClass
Koin AnnotationsDI module declarations
ComposeUI component metadata
// kotlinx.serialization — KSP generates the serializer
@Serializable
data class User(
val name: String,
val email: String,
@SerialName("created_at") val createdAt: Instant
)
// At compile time, KSP generates User.serializer() which knows
// how to convert User to/from JSON, Protobuf, CBOR, etc.
// No runtime reflection!
val json = Json.encodeToString(User.serializer(), user)
val user = Json.decodeFromString<User>(json) // reified generic!

Kotlin Multiplatform (KMP) lets you share code between JVM, JS, Native, iOS, and WASM targets. The expect/actual mechanism declares platform-specific APIs that each target must implement: common code holds the expect declarations, and every target provides its own actual.

expect/actual across targets
Rendering diagram…

An expect fun or expect class is like a header — it declares what exists, and each target’s actual supplies the implementation:

// --- commonMain/kotlin/Platform.kt ---
// `expect` declares WHAT exists (like an interface / header file)
expect fun currentTimeMillis(): Long
expect class UUID {
companion object {
fun randomUUID(): UUID
}
fun toHexString(): String
}
expect fun readEnvironmentVariable(name: String): String?
// --- jvmMain/kotlin/Platform.jvm.kt ---
// `actual` provides the JVM implementation
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
actual class UUID(private val javaUuid: java.util.UUID) {
actual companion object {
actual fun randomUUID(): UUID = UUID(java.util.UUID.randomUUID())
}
actual fun toHexString(): String = javaUuid.toString().replace("-", "")
}
actual fun readEnvironmentVariable(name: String): String? = System.getenv(name)
// --- jsMain/kotlin/Platform.js.kt ---
// `actual` provides the JavaScript implementation
actual fun currentTimeMillis(): Long = js("Date.now()").unsafeCast<Double>().toLong()
actual class UUID(private val value: String) {
actual companion object {
actual fun randomUUID(): UUID = UUID(js("crypto.randomUUID()").unsafeCast<String>())
}
actual fun toHexString(): String = value.replace("-", "")
}
actual fun readEnvironmentVariable(name: String): String? =
js("process.env[name]").unsafeCast<String?>()
// TS handles multi-platform with conditional imports or runtime checks
// Not compile-time safe!
// Option 1: Different entry points
// node.ts
export const readFile = (path: string) => fs.readFileSync(path, "utf-8");
// browser.ts
export const readFile = (path: string) => {
throw new Error("File reading not supported in browser");
};
// Option 2: Runtime detection
const isNode = typeof process !== "undefined" && process.versions?.node;
const readFile = isNode
? require("fs").readFileSync
: () => { throw new Error("Not supported"); };

Key differences:

  • TypeScript has no compile-time multiplatform — you use bundler configs or runtime checks.
  • Go build tags are string-based, not type-checked across platforms.
  • Kotlin’s expect/actual is fully type-checked at compile time — missing implementations are compile errors.

A KMP build declares its targets and per-target source sets:

build.gradle.kts
plugins {
kotlin("multiplatform") version "2.0.0"
}
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
// Native targets
linuxX64()
macosX64()
macosArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-cio:3.0.0")
}
}
val jsMain by getting {
dependencies {
implementation("io.ktor:ktor-client-js:3.0.0")
}
}
}
}

The directory layout mirrors those source sets — shared code in commonMain, with each platform’s actual implementations in its own folder:

  • Directorysrc/
    • DirectorycommonMain/kotlin/ shared code (expect declarations, pure logic)
      • Platform.kt
      • Directorymodel/
        • User.kt @Serializable data classes
      • Directoryservice/
        • UserService.kt business logic (pure Kotlin)
    • DirectorycommonTest/kotlin/ shared tests
    • DirectoryjvmMain/kotlin/ JVM-specific (actual implementations)
      • Platform.jvm.kt
    • DirectoryjsMain/kotlin/ JS-specific
      • Platform.js.kt
    • DirectorynativeMain/kotlin/ Native-specific
      • Platform.native.kt

The real power of KMP is sharing business logic — validation, models, an API client — across platforms, with only the truly platform-specific bits behind expect:

// --- commonMain ---
// Pure business logic — no platform dependencies
@Serializable
data class User(val id: String, val name: String, val email: String)
@Serializable
data class CreateUserRequest(val name: String, val email: String)
class UserValidator {
fun validate(request: CreateUserRequest): List<String> {
val errors = mutableListOf<String>()
if (request.name.isBlank()) errors.add("Name is required")
if (request.name.length > 100) errors.add("Name too long")
if (!request.email.contains("@")) errors.add("Invalid email")
return errors
}
}
// This can be used on JVM backend, JS frontend, and iOS/Android clients
// Same validation logic everywhere — no duplication!
// Platform-specific HTTP client
expect class HttpClient() {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}
// Shared service using the platform-specific client
class UserApiClient(private val baseUrl: String) {
private val client = HttpClient()
private val json = Json { ignoreUnknownKeys = true }
suspend fun getUser(id: String): User {
val response = client.get("$baseUrl/users/$id")
return json.decodeFromString(response)
}
suspend fun createUser(request: CreateUserRequest): User {
val body = json.encodeToString(request)
val response = client.post("$baseUrl/users", body)
return json.decodeFromString(response)
}
}
FeatureTypeScript EquivalentGo EquivalentKotlin
DSL buildersMethod chaining / tagged templatesFunctional optionsLambda with receiver + @DslMarker
Class delegationManual forwardingStruct embeddingclass Foo : Bar by impl
Property delegationgetter/setter, decoratorsNo equivalentvar x by lazy { }
Reified genericsNot possible (types erased)Type assertionsinline fun <reified T>
Context parametersConstructor injectioncontext.Contextcontext(Foo)
Code generationDecorators (runtime)go generateKSP (compile-time)
MultiplatformRuntime checks, bundlerBuild tagsexpect/actual

What to remember:

  • Kotlin DSLs are not magic — they combine extension functions, lambdas with receivers, and @DslMarker.
  • Delegation (by) replaces inheritance and manual forwarding with zero-cost composition.
  • inline + reified solves the JVM type erasure problem at compile time.
  • Context receivers/parameters are Kotlin’s answer to “ambient” dependencies.
  • KSP generates code at compile time, avoiding runtime reflection overhead.
  • Kotlin Multiplatform lets you share real business logic across JVM, JS, and Native.

Put these patterns to work — both exercises build the kind of small, expressive library that shows off Kotlin’s metaprogramming features.