Kotlin Basic Syntax


Kotlin Foundations for Backend Development

When transitioning to Kotlin, focus on the idioms and patterns most relevant to building reliable services. Rather than memorizing the entire language specification, prioritize the syntax that appears most frequently in backend engineering: variable declarations, functions, control flow, and string templates.


var and val: Immutability as a Default

var and val are design primitives that determine how much state mutability your system allows.

// val: Immutable request objects ensure thread safety
data class CreateUserRequest(
    val email: String,
    val nickname: String
)

class UserTokenIssuer(
    private val tokenSecret: String // val: Immutable once injected
) {
    fun issue(userId: Long): String {
        val issuedAt = System.currentTimeMillis() // val: Calculated once
        return "$userId:$issuedAt:$tokenSecret"
    }
}

Favoring val for request objects and intermediate state increases predictability and reduces the surface area for bugs in concurrent environments. Use var only when state mutation is strictly necessary, such as for counters or accumulators.

fun countProcessedEvents(events: List<String>): Int {
    var processedCount = 0 // var: Mutation is required for accumulation
    for (event in events) {
        if (event.isNotBlank()) {
            processedCount++
        }
    }
    return processedCount
}

Keep the scope of var as narrow as possible.


Functions: Default Values, Named Arguments, and Single-Expressions

Explicitly declaring return types for public APIs is a best practice for maintainability.

class PaymentService {
    // Block body: Clear sequence for complex logic
    fun calculateVat(amount: Long): Long {
        return (amount * 0.1).toLong()
    }

    // Expression body (=): Concise one-liners for simple transforms
    fun normalizeEmail(raw: String): String = raw.trim().lowercase()
}

Use expression bodies (=) for simple conversions and block bodies for logic involving branching or validation. Avoid over-complicating functions; separate verification, transformation, and persistence to ensure testability.


if and when: Statements vs. Expressions

Kotlin’s if and when function as both statements and value-returning expressions. Using them as expressions reduces intermediate state and allows the compiler to verify branch coverage.

enum class OrderStatus { PENDING, PAID, CANCELLED }

// when as an expression: Compiler ensures all enum cases are handled
fun describeStatus(status: OrderStatus): String = when (status) {
    OrderStatus.PENDING   -> "Awaiting Payment"
    OrderStatus.PAID      -> "Payment Completed"
    OrderStatus.CANCELLED -> "Canceled"
}

// if as an expression: Direct return without temporary variables
fun discountRate(userLevel: Int): Double = if (userLevel >= 5) 0.2 else 0.1

Expression-based when prevents bugs caused by missing else branches or unhandled cases.


String Templates: Value Injection without Concatenation

String templates use $variable or ${expression} to inject values directly into strings, ideal for logging, formatting, and message generation.

data class User(val id: Long, val name: String, val grade: String)

fun greet(user: User): String {
    return "Hello, ${user.name}! Your current grade is ${user.grade}." // ${expression}: property access
}

fun summarize(user: User, orderCount: Int): String {
    val suffix = if (orderCount >= 10) "Premier" else "Regular"
    return "[userId=${user.id}] $suffix (Orders: $orderCount)" // $variable: direct injection
}

This syntax eliminates the need for the + operator, significantly improving readability.


Iteration and Range: for, while, and Collections

Kotlin’s for loop supports various range expressions (.., until, downTo, step).

// 1 to 5 (inclusive)
for (i in 1..5) print("$i ") // 1 2 3 4 5

// 1 to 4 (exclusive of end)
for (i in 1 until 5) print("$i ") // 1 2 3 4

// Reverse from 5 to 1, step by 2
for (i in 5 downTo 1 step 2) print("$i ") // 5 3 1

// De-structuring iteration
val items = listOf("a", "b", "c")
for ((index, value) in items.withIndex()) {
    println("$index: $value")
}

While while is used for manual condition control, collection operations are often more idiomatic for filtering and mapping.

data class OrderEvent(val orderId: Long, val status: String)

fun extractPaidOrderIds(events: List<OrderEvent>): List<Long> {
    return events
        .asSequence() // Lazy evaluation to avoid intermediate allocations
        .filter { it.status == "PAID" }
        .map { it.orderId }
        .toList() // Terminal operation triggers execution
}

Use asSequence() for large datasets to reduce GC pressure by avoiding intermediate collection allocations. For small lists, standard collection operations are usually sufficient.


Keywords: in, is, as?, object, and companion object

These keywords are essential for common Kotlin backend patterns.

sealed interface Principal
data class AdminPrincipal(val adminId: Long): Principal
data class UserPrincipal(val userId: Long): Principal

class AuthContext private constructor(
    private val allowedRoles: Set<String>
) {
    companion object {
        // Factory method accessible via class name (AuthContext.of(...))
        fun of(vararg roles: String): AuthContext = AuthContext(roles.toSet())
    }

    // in: Membership check
    fun canAccess(role: String): Boolean = role in allowedRoles
}

fun extractUserId(principal: Principal): Long? {
    return when (principal) {
        is UserPrincipal  -> principal.userId // is: Type check + Smart cast
        is AdminPrincipal -> principal.adminId
    }
}

fun parseCount(raw: Any): Int? {
    val asString = raw as? String ?: return null // as?: Safe cast, returns null on failure
    return asString.toIntOrNull()
}


Control Flow: break, continue, and Labeled Returns

Use break and continue for manual loop control, and labeled returns for early exits in higher-order functions.

fun pollMessages(maxRetry: Int): Boolean {
    var retry = 0
    while (retry < maxRetry) {
        val received = receiveFromQueue()
        if (received == null) {
            retry++
            continue // Skip to next iteration
        }
        if (received == "STOP") break // Exit loop
        return true
    }
    return false
}

fun hasNegative(values: List<Int>): Boolean {
    values.forEach loop@{ value ->
        if (value < 0) return@loop // Labeled return: exits only the lambda
    }
    return values.any { it < 0 }
}


try-catch-finally: Structured Exception Handling

Separate concerns within exception blocks: try for the happy path, catch for classification/logging, and finally for resource cleanup.

fun readRequiredEnv(name: String): String {
    try {
        val value = System.getenv(name)
        if (value.isNullOrBlank()) {
            throw IllegalStateException("Missing env: $name")
        }
        return value
    } catch (t: Throwable) {
        val retryable = t is java.io.IOException // Type-based classification
        println("Env read failed: key=$name, retryable=$retryable, cause=${t.message}")
        throw t
    } finally {
        println("Env lookup finished: key=$name") // Always executes
    }
}

Avoid heavy I/O in finally to prevent blocking failure paths. Centralize cleanup logic for clarity.

Doing heavy I/O in finally can result in slower failure paths. It is safer to just do the cleanup work and separate the logic into a separate function.


Next Steps: Advancing to Type Systems

Building a solid foundation with these basics allows for more productive engineering. Focus on writing clean functions and managing state safely. The next post explores Kotlin’s advanced type system, including constructor validation, interfaces, sealed classes, and generics.


References