Skip to content

Top 30 Kotlin Interview Questions and Answers [2026]

October 2, 2026 14 min read
Kotlin interview guide by Second Talent
TL;DR: Kotlin interviews in 2026 test null safety, sealed types, coroutines and structured concurrency, Flow, and how Kotlin works with Spring and Java. Candidates should know that Kotlin 2.4 made context parameters stable, and that the K2 compiler has been the default since Kotlin 2.0.

Kotlin 2.4.0, released on June 3, 2026, made context parameters stable after two releases as an experimental feature. It also added collection literals such as ["a", "b"] as an experimental option.

The current patch is 2.4.20, from September 7, 2026. Most interviews still turn on the same core: null safety, coroutines and clean interop with Java.

Key takeaways
  1. 1The current coroutines library is kotlinx.coroutines 1.11.0, released on May 8, 2026.
  2. 2Spring Boot 4.0 (November 2025) moved to Kotlin 2.2 and added a starter for kotlinx.serialization.
  3. 3Kotlin 2.3 added an unused return value checker that warns when a function's result is silently dropped.
  4. 4Dispatchers.IO allows 64 threads or the number of cores, whichever is larger, by default.

Kotlin Language

1. What is the difference between val, var and const val?

var can be reassigned, val cannot, and const val is a compile-time constant. A val can be set from a function call at runtime.

A const val must be a primitive or String known at compile time, declared at the top level or in an object, and the compiler inlines its value.

A val is read-only, not immutable. val items = mutableListOf(1) can still have items added. And a val with a custom getter can return a new value on every read.

2. How does null safety work, and where does it break down?

Types are non-null by default. String? allows null, and the compiler forces you to handle it before use.

The tools are the safe call ?., the Elvis operator ?: for a fallback, smart casts after a null check, and !!, which throws if the value is null.

val length = user?.email?.length ?: 0

The gap is Java. A value from Java code without nullability annotations has a "platform type" such as String!, and Kotlin trusts you with it. Assigning it to a non-null String can throw at runtime.

Good code declares the expected type at the Java boundary. Frequent !! in a code review is a warning sign.

3. What does a data class generate, and what are its limits?

A data class gets equals, hashCode, toString, componentN functions for destructuring, and copy, all from the properties in the primary constructor.

Properties declared in the class body are left out of all of them, which surprises people when two objects compare equal. copy is shallow: a copied object shares the same list instance as the original.

Data classes also make poor JPA entities, because generated equals and hashCode clash with lazy loading and generated IDs.

4. What are sealed classes and interfaces for?

A sealed type has a fixed set of direct subtypes, known at compile time, so a when over it needs no else branch. Adding a new subtype then turns every incomplete when into a compile error, which is the point.

sealed interface PaymentResult {
    data class Paid(val id: String) : PaymentResult
    data class Declined(val reason: String) : PaymentResult
    data object Pending : PaymentResult
}

fun message(r: PaymentResult) = when (r) {
    is PaymentResult.Paid -> "Paid ${r.id}"
    is PaymentResult.Declined -> "Declined: ${r.reason}"
    PaymentResult.Pending -> "Pending"
}

A common outdated answer says subclasses must be in the same file. Per the sealed classes docs, direct subclasses must be in the same package and module.

5. What do inline, reified, noinline and crossinline do?

inline copies a function's body, and the lambdas passed to it, into each call site. That removes the lambda object allocation and allows a return inside the lambda to return from the calling function.

  • reified keeps a type parameter available at runtime in an inline function, so T::class and is T work.
  • noinline marks one lambda parameter that must stay an object, for example to store it.
  • crossinline forbids a non-local return in a lambda that runs in another context, such as inside a Runnable.
inline fun <reified T> String.parse(): T = Json.decodeFromString<T>(this)

6. How do you choose between let, run, with, apply and also?

By two questions: is the object this or it inside the block, and does the block return the object or the lambda's result?

  • apply (this, returns the object): configure an object.
  • also (it, returns the object): side effects such as logging.
  • let (it, returns the result): run code on a non-null value with ?.let.
  • run and with (this, return the result): compute a value from an object.

A strong candidate also says when not to use them: nested scope functions make it hard to tell what it and this refer to.

7. What is a value class?

A value class wraps a single value in its own type, and the compiler usually removes the wrapper at runtime. It gives type safety without an extra object.

@JvmInline
value class UserId(val raw: Long)

fun load(id: UserId) { /* cannot be passed an OrderId by mistake */ }

The wrapper is kept (boxed) when the value is used as a nullable type, a generic type or an interface. Function names that take value classes are mangled in bytecode, so Java callers need @JvmName or a plain overload.

8. What do out, in and star projection mean in generics?

out T means a type only produces T, so List<String> can be used as List<Any>. in T means it only consumes T, so a Comparator<Any> can be used as a Comparator<String>.

Kotlin declares variance on the class (interface List<out E>), where Java uses wildcards at every use. Star projection, List<*>, means "a list of some type I don't know". You can read items as Any? but cannot add to it.

Coroutines

9. What does the suspend keyword actually do?

It lets a function pause without blocking its thread and resume later. The compiler adds a hidden Continuation parameter and turns the function into a state machine that can stop at each suspension point.

suspend does not make code non-blocking by itself. A suspend fun that calls Thread.sleep or a blocking JDBC driver still blocks its thread. Blocking calls belong inside withContext(Dispatchers.IO).

10. What is the difference between launch and async?

launch starts a coroutine for its side effects and returns a Job. async starts one that computes a value and returns a Deferred<T>, which you read with await().

suspend fun loadPage(id: Long) = coroutineScope {
    val user = async { users.find(id) }
    val orders = async { orders.forUser(id) }
    Page(user.await(), orders.await())   // both calls run at the same time
}

They also differ in errors. launch reports an exception to its parent at once. async also cancels its parent when it is a child, but a root async keeps the exception until someone calls await().

11. What is structured concurrency?

Every coroutine runs inside a scope, and a scope does not finish until all its children finish. Cancelling a scope cancels its children, and a failing child cancels its parent.

No coroutine is left running after the work that started it is gone.

Flowchart of a failing child coroutine: if the parent is a supervisorScope or SupervisorJob only that child fails; otherwise the parent cancels every other child and fails with the exception.

coroutineScope { } applies the default rule: one failure cancels the siblings and is rethrown. supervisorScope { } lets children fail on their own, which suits independent tasks such as sending several notifications.

The exception handling docs cover both.

12. Which dispatcher should run which work?

Dispatchers.Default is for CPU work, and its pool is sized to the number of cores. Dispatchers.IO is for blocking I/O. Per the Dispatchers.IO docs, it allows 64 threads or the number of cores, whichever is larger.

Dispatchers.Main exists on UI platforms such as Android.

For a resource with its own limit, such as a pool of 10 database connections, use Dispatchers.IO.limitedParallelism(10). That view gets its own limit, so it does not starve other I/O work.

Dispatchers.Unconfined is rarely the right answer in application code.

13. How does cancellation work, and what is the runCatching trap?

Cancellation is cooperative. A cancelled coroutine gets a CancellationException at its next suspension point, such as delay or await. CPU loops must check isActive or call ensureActive(), or they run to the end.

The trap: runCatching { } or catch (e: Exception) around suspend calls also catches CancellationException. The coroutine then carries on as if it were never cancelled. Rethrow it, or catch narrower exception types.

Cleanup after cancellation. Code in a finally block cannot suspend once the coroutine is cancelled. Wrap suspending cleanup, such as closing a remote session, in withContext(NonCancellable) { }.

14. When does a CoroutineExceptionHandler run?

Only for uncaught exceptions in root coroutines started with launch. A handler on a child coroutine is ignored, because the child passes its exception to the parent. async never uses it, because the exception is delivered through await().

So the handler is a last-resort logger on a top-level scope, not a way to recover. Recovery belongs in try and catch around the code that can fail.

15. Why avoid GlobalScope and runBlocking?

GlobalScope starts coroutines that belong to no parent, so nothing cancels them or waits for them. They leak when the request or screen that started them is gone. The library marks it as a delicate API.

runBlocking blocks the current thread until its coroutine finishes. It fits a main function or a test. Inside a server request or a suspend function it blocks a thread the dispatcher needs, and it can deadlock a small thread pool.

Use a scope tied to a lifecycle instead, such as Spring's request handling or a service-owned CoroutineScope.

16. How do you protect shared mutable state between coroutines?

Coroutines on Default or IO run on several threads, so shared state needs protection. The options, lightest first:

  • Atomics for one counter or reference.
  • Mutex with withLock { }, which suspends instead of blocking a thread while it waits.
  • Confinement: run all updates on limitedParallelism(1), so only one runs at a time.
  • MutableStateFlow.update { }, which applies a change atomically.

A JVM synchronized block must never contain a suspension point.

Flow

17. What is the difference between a cold and a hot flow?

A cold flow runs its producer code once for each collector, starting when collect is called. A hot flow emits whether or not anyone collects, and collectors share one source.

Two-column comparison of cold flow { } and hot StateFlow or SharedFlow: cold flows start on collect and run once per collector; hot flows emit without collectors and share one source.

flow { } builds a cold flow; StateFlow and SharedFlow are hot. stateIn and shareIn turn a cold flow into a hot one within a scope. That is how you avoid running one database query per subscriber.

18. When do you use StateFlow and when SharedFlow?

Use StateFlow for state: it always has a current value, a new collector gets it at once, and equal values in a row are skipped.

Use SharedFlow for events such as "show a message", where every emission matters and there may be no current value.

The classic bug is sending one-off events through StateFlow. Two identical events in a row collapse into one, and a new collector replays the last event. SharedFlow lets you choose replay and buffer settings to match.

StateFlow
  • Always has a current value
  • Skips a value equal to the last one
  • Screen state, connection status
SharedFlow
  • No value required; replay is set by you
  • Delivers repeated equal values
  • One-off events, messages, ticks

19. How does flowOn work, and why can't you switch context inside flow { }?

flowOn(Dispatchers.IO) changes the dispatcher for the operators above it, and the collector keeps its own context. Flow guarantees "context preservation": values reach the collector in the collector's context.

Calling withContext around emit inside flow { } breaks that rule and throws an exception at runtime. To emit from other coroutines, use channelFlow { }, which is built for concurrent producers.

20. How do you test coroutines and flows?

Use runTest from kotlinx-coroutines-test. It runs the test on a test dispatcher with virtual time, so delay(60_000) finishes at once. advanceTimeBy and advanceUntilIdle control the clock.

Code under test should take its dispatcher as a parameter rather than hard-coding Dispatchers.IO, so tests can pass a StandardTestDispatcher.

Flows are easy to test by collecting to a list with toList(), or with the Turbine library for step-by-step checks.

Backend and Java Interop

21. Why does Kotlin with Spring need the kotlin-spring and kotlin-jpa plugins?

Kotlin classes are final by default, but Spring creates proxies by subclassing @Configuration, @Transactional and similar classes. The kotlin-spring compiler plugin (a preset of all-open) opens classes that carry those annotations.

JPA needs a no-argument constructor on entities. The kotlin-jpa plugin (a preset of no-arg) generates one. Projects from start.spring.io include both. Without them, you get confusing proxy errors or entities that Hibernate cannot create.

22. How do coroutines work in Spring controllers?

Spring MVC and WebFlux both accept suspend handler methods and Flow return values in annotated controllers. That is stated in Spring's coroutines reference. WebFlux also has a coRouter { } DSL for functional endpoints.

@GetMapping("/users/{id}")
suspend fun user(@PathVariable id: Long): UserDto = service.find(id)

The benefit depends on the stack below. On WebFlux with R2DBC or a non-blocking HTTP client, a suspended request holds no thread.

On Spring MVC over blocking JDBC, a thread is still tied up during the query, so the gain is mostly cleaner code.

23. Why choose kotlinx.serialization over Jackson?

kotlinx.serialization generates serializers at compile time with a compiler plugin, so there is no reflection. It understands Kotlin defaults and nullability, and it works on every Kotlin platform.

A missing @Serializable is a compile error rather than a runtime surprise.

Jackson has a larger feature set and better support for Java types. Spring Boot 4.0 added a spring-boot-starter-kotlin-serialization starter, so either can be the default in a Spring app.

A strong candidate says which they picked and why, for example "kotlinx for a multiplatform client, Jackson for a Spring service with many Java DTOs".

24. How does Ktor structure a server?

A Ktor server is an application with plugins installed on it. Plugins handle routing, content negotiation, authentication, CORS and logging, and each one hooks into the request pipeline.

Early Ktor called these "features"; version 2 renamed them plugins. The current release is 3.6.0, from September 18, 2026.

fun Application.module() {
    install(ContentNegotiation) { json() }
    routing {
        get("/health") { call.respondText("ok") }
    }
}

Ktor is coroutine-based from the ground up, with no blocking thread-per-request model. It suits small services that want little framework. Spring suits teams that want its ecosystem and conventions.

What Changed Recently

May 21, 2024
Kotlin 2.0: the K2 compiler becomes stable
Jun 23, 2025
Kotlin 2.2: guard conditions stable, context parameters in preview
Dec 16, 2025
Kotlin 2.3: unused return value checker, explicit backing fields
Jun 3, 2026
Kotlin 2.4: context parameters stable, collection literals
Sep 7, 2026
Kotlin 2.4.20: coroutine stack trace recovery in the standard library

25. What did the K2 compiler in Kotlin 2.0 change?

K2 is a rewritten compiler front end, and it became stable and the default in Kotlin 2.0, released on May 21, 2024, per the Kotlin 2.0 release notes. It is one architecture for JVM, Native, JS and Wasm, and it compiles faster.

For day-to-day code, the visible change is smarter smart casts. After if (x is Cat || x is Dog), x is now cast to their closest common supertype, not Any. Smart casts also survive inside inline function lambdas.

Every language feature added since, including context parameters, was built on K2. A project still on 1.9 cannot use any of them.

26. What became stable in Kotlin 2.2?

Guard conditions in when, non-local break and continue, and multi-dollar string interpolation. All three are listed as stable in the Kotlin 2.2 release notes.

when (event) {
    is Order.Placed if event.total > 1_000 -> flagForReview(event)
    is Order.Placed -> confirm(event)
    is Order.Cancelled -> refund(event)
}

val schema = $$"""{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "$${name}" }"""

A guard adds an if condition to a branch without nesting. Non-local break and continue work inside lambdas passed to inline functions such as forEach. With $$ before a string, only $$ starts a template, so a literal $ needs no escaping.

27. What are context parameters, and what did they replace?

Context parameters let a function require a value from its caller's context without passing it as a normal argument.

They are stable in Kotlin 2.4, except for explicit context arguments and callable references, per the Kotlin 2.4 release notes.

context(tx: Transaction)
fun saveOrder(order: Order) {
    tx.insert(order)
}

transaction { saveOrder(order) }   // Transaction comes from the enclosing scope

They replace the older experimental context receivers. The key difference is naming: a context parameter has a name, such as tx, and you call its members through it.

Context receivers made members available implicitly, which made code hard to read. Code using context receivers needs to migrate.

28. What are explicit backing fields?

They let a property declare a private field of a more specific type than its public type, with no second property. They arrived in Kotlin 2.3 as experimental and are stable in 2.4.

The common use is exposing a read-only flow while keeping a mutable one inside.

// Before
private val _city = MutableStateFlow("")
val city: StateFlow<String> get() = _city

// Kotlin 2.4
val city: StateFlow<String>
    field = MutableStateFlow("")

Inside the class, city is smart-cast to MutableStateFlow, so city.value = "Hanoi" works there. Outside, callers only see StateFlow. The old _city naming pattern goes away.

29. What does the unused return value checker catch?

It warns when an expression returns something other than Unit or Nothing and the result is never used. Kotlin 2.3 introduced it, per the Kotlin 2.3 release notes, and you turn it on with -Xreturn-value-checker=check.

The classic bug it finds is list.map { it.trim() } or text.replace("a", "b") written as if they changed the value in place. Both return a new value, and dropping it means the code does nothing.

Kotlin 2.4 extended the check to results of higher-order functions.

30. What are collection literals in Kotlin 2.4?

Collection literals create collections with square brackets, such as val tags: Set<String> = ["kotlin", "jvm"]. The expected type decides what is built, and without one the result is a List.

The feature is experimental in Kotlin 2.4 and needs a compiler option to enable.

Per the release notes, literals cannot yet build collections defined in Java. Because it is experimental, the syntax may still change, so production code should keep using listOf and setOf for now.

A candidate who knows the difference between stable and experimental features is a good sign.

Signs of a Strong Answer

  • They notice runCatching or catch (e: Exception) around suspend calls and explain why it breaks cancellation.
  • They explain that suspend alone does not make blocking code safe, and move JDBC calls to Dispatchers.IO.
  • They pick SharedFlow for one-off events and can describe the bug StateFlow causes there.
  • They treat platform types from Java as untrusted and declare nullability at the boundary.
  • They know sealed subclasses must share a package and module, not a file.
  • They can say which recent features are stable (context parameters, explicit backing fields) and which are still experimental (collection literals).

Hiring Kotlin Developers

Many strong Kotlin engineers work remotely from Asia. Second Talent matches companies with pre-vetted back-end engineers, including Kotlin specialists, screened with questions like these.

Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our Spring Boot and Android interview guides.

Hiring developers in Southeast Asia?

Get Cost Guide

How would you like to talk?

WhatsApp us Prefer texting at your own pace? Just hit us up on WhatsApp. We promise no spam and a hassle-free experience.

Loading available times…