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.
val user = mutableListOf("Alice", "Bob")
user.add("Charlie") // OK—the list is mutable
user = mutableListOf("David") // Compile error—cannot reassign valvar declares a mutable variable. It can be reassigned at any time, but the type must remain the same.
var count = 10
count = 20 // OK
count = "twenty" // Compile error—type mismatchconst 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.
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 constantIn 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.
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 unchangedThis 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.
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 trueStore 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.
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.
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:
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:
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:
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 Listis immutable at the reference level, but if it's aMutableList, 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
mapandfilterin stdlib, but be conservative in business logic. - Forgetting that const must be at top-level or in object. Trying to use
constinside a class (not companion object) is a common slip-up during interviews.
Key takeaways
- Default to
val; it's safer and clearer. Usevaronly when state genuinely changes. Reserveconstfor 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
reifiedrecover 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.






Comments
Loading comments…