Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 079 / 100

Kotlin interview questions: patterns, pitfalls and fixes

Language questions you will hear.

Expona Admin7 min read

Introduction

You're sitting across from an interviewer at a fintech startup, and they ask: "Walk me through the difference between val, var, and const." Kotlin interview questions on patterns, pitfalls and fixes often separate engineers who've shipped production code from those who've only read tutorials. We've conducted dozens of technical interviews and reviewed hundreds of pull requests—the candidates who understand not just the syntax but the why behind Kotlin's design choices consistently move forward. This guide covers the language fundamentals you'll encounter, with real scenarios from our experience building trading platforms, e-commerce services, and SaaS backends.

val vs var vs const

This question comes up in almost every Kotlin interview, and the answer reveals whether you understand immutability and scope.

val declares a read-only variable. Once assigned, it cannot be reassigned. However, the object it references can still be mutated if that object is mutable.

kotlin
val user = mutableListOf("Alice", "Bob")
user.add("Charlie")  // OK—the list is mutable
user = mutableListOf("David")  // Compile error—cannot reassign val

var declares a mutable variable. It can be reassigned at any time, but the type must remain the same.

kotlin
var count = 10
count = 20  // OK
count = "twenty"  // Compile error—type mismatch

const declares a compile-time constant. It must be initialized with a value known at compile time, can only hold primitive types or strings, and must be a top-level declaration or inside an object.

kotlin
const val MAX_RETRIES = 3  // Top-level constant
const val BUILD_VERSION = "1.0.0"

class Config {
    companion object {
        const val TIMEOUT_MS = 5000  // OK—inside object
    }
}

val timestamp = System.currentTimeMillis()  // Cannot use const—not compile-time constant

In production, we default to val. It signals intent, prevents accidental reassignment, and works with smart casts. Reserve var for loop indices, mutable accumulators, and state that genuinely changes. Use const only for configuration that won't change between builds.

Data classes and copy

Data classes are purpose-built for holding data, and they generate equals(), hashCode(), toString(), and copy() automatically. The copy() function is particularly powerful for immutable updates.

kotlin
data class Order(
    val id: String,
    val userId: String,
    val status: String,
    val total: Double
)

val order = Order("ORD-001", "USER-42", "pending", 199.99)
val updated = order.copy(status = "confirmed")
// updated = Order("ORD-001", "USER-42", "confirmed", 199.99)
// order remains unchanged

This is ideal for event-driven architectures and state management. In our e-commerce platform, we use copy() constantly when modifying orders without mutating shared state.

A subtle but critical point: data classes generate equals() based on all primary constructor parameters. If you add mutable properties outside the constructor, they won't be considered in equality checks.

kotlin
data class User(val id: String, val name: String) {
    var lastLogin: Long = 0  // Not part of equals()
}

val u1 = User("1", "Alice")
val u2 = User("1", "Alice")
println(u1 == u2)  // true, even if lastLogin differs
u1.lastLogin = System.currentTimeMillis()
println(u1 == u2)  // still true

Store mutable state in separate objects or use var in the primary constructor only if the field truly belongs in equality calculations. In payment processing systems, we keep a data class immutable and wrap mutable state separately—no surprises when comparing transaction records.

Sealed classes vs enums

Both restrict which subclasses can exist, but they solve different problems.

Enums are for a fixed set of named constants, all of the same type, typically with shared behavior.

kotlin
enum class PaymentStatus {
    PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
}

enum class Color(val hex: String) {
    RED("#FF0000"),
    GREEN("#00FF00"),
    BLUE("#0000FF")
}

Sealed classes are for a restricted hierarchy where each subclass can have different properties and methods. They're perfect for representing discriminated unions or result types.

kotlin
sealed class Result {
    data class Success(val data: T) : Result()
    data class Error(val exception: Exception) : Result()
    object Loading : Result()
}

fun fetchUser(id: String): Result {
    return try {
        Result.Success(userService.getUser(id))
    } catch (e: Exception) {
        Result.Error(e)
    }
}

// When expressions are exhaustive with sealed classes
when (val result = fetchUser("USER-42")) {
    is Result.Success -> println("User: ${result.data}")
    is Result.Error -> println("Error: ${result.exception}")
    is Result.Loading -> println("Loading...")
}

Sealed classes guarantee exhaustive when expressions—the compiler won't let you forget a case. We use them heavily in ride-hailing backends to represent trip states, where each state has different properties: a Booked state has a driver assignment, while Searching does not.

The key distinction: use enums when you need a small, fixed set of identical values; use sealed classes when each variant is structurally different.

Inline functions and reified types

Inline functions eliminate function call overhead by copying their bytecode to the call site. Combined with reified type parameters, they solve Java's type erasure problem.

In Java and standard Kotlin, generic type information is erased at runtime. This fails:

kotlin
fun  parseJson(json: String): T {
    // T is erased—we don't know what type T is at runtime
    return JSON.parse(json, T::class.java)  // Compile error
}

With an inline function and reified, the type is available at runtime:

kotlin
inline fun  parseJson(json: String): T {
    return JSON.parse(json, T::class.java)  // Works
}

val user: User = parseJson("""{"id":"1","name":"Alice"}""")

reified only works on inline functions because the compiler copies the function body to each call site, substituting the actual type. Standard function calls can't do this.

Inline functions can improve performance in tight loops, but they increase bytecode size. Use them for small, frequently called functions or when you need type information:

kotlin
inline fun  isInstance(obj: Any): Boolean {
    return obj is T
}

inline fun  List.filterIsInstance(): List {
    return filter { it is T }.map { it as T }
}

val mixed: List = listOf(1, "hello", 2.5, "world")
val strings = mixed.filterIsInstance()  // ["hello", "world"]

Don't over-inline. In our SaaS backend, we keep most functions non-inline and reserve inlining for JSON serialization helpers and type-checking utilities where the benefit is measurable.

Common mistakes

  • Confusing immutability with unchangeable references. A val List is immutable at the reference level, but if it's a MutableList, the contents can change. Interviewers often probe this to see if you think layered.
  • Using data classes when you need custom equality. If only certain fields should affect equality, don't force it into the primary constructor—override equals() explicitly or reconsider your data model.
  • Overcomplicating sealed classes. New engineers sometimes nest sealed classes unnecessarily. Sealed classes should represent a closed set of alternatives; if the hierarchy is open or deeply nested, you may want standard inheritance or a different pattern.
  • Inlining for the wrong reasons. Inlining doesn't magically make code faster; it trades bytecode size for potential speed gains. Profile before optimizing. Inline liberally with higher-order functions like map and filter in stdlib, but be conservative in business logic.
  • Forgetting that const must be at top-level or in object. Trying to use const inside a class (not companion object) is a common slip-up during interviews.

Key takeaways

  • Default to val; it's safer and clearer. Use var only when state genuinely changes. Reserve const for compile-time configuration.
  • Data classes + copy() enable functional updates and are natural for immutable domain models. Keep mutable state outside primary constructors for clarity.
  • Sealed classes provide type-safe discriminated unions and exhaustive when checks; enums are for fixed sets of identical constants.
  • Inline functions with reified recover type information lost to erasure, but use them judiciously—measure performance gains and watch for bytecode bloat.
  • These aren't academic distinctions; they shape how you structure APIs, handle errors, and reason about state in production systems.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles