Kotlin's compiler hates nulls more than you do
Kotlin interviews do not test whether you can write Kotlin. They test whether you understand why the compiler retires entire classes of Java bugs — null pointers, unchecked async leaks, non-exhaustive when branches. Here is what Google, Meta, Uber, Lyft, and Spotify probe — from platform types to structured concurrency — and how to answer without hand-waving.
Kotlin "explicitly supports nullability as part of its type system, meaning you can explicitly declare which variables or properties are allowed to be null. Also, when you declare non-null variables, the compiler enforces that these variables cannot hold a null value, preventing an NPE." That is the official Kotlin documentation on null safety. If you cannot reframe every Kotlin question as "what is the compiler proving here," you are interviewing for the wrong language.
Kotlin is a modern language that runs on the Java Virtual Machine and compiles down to the same bytecode Java does. The thing every interviewer probes is null safety: Kotlin forces you to mark nullable types with a ?, and the compiler refuses to let you forget. That one feature retires most NullPointerExceptions at compile time instead of in production.
Kotlin interviews do not ask you to write Kotlin. They ask you to prove you understand why the compiler retires entire classes of Java bugs before they run.
The real question is which kind of Kotlin shop you are interviewing at. Google treats Kotlin as the first-class Android language with AOSP-scale tooling expectations. Meta, Uber, Lyft, and Spotify treat it as the language for product Android, and still probe the language semantics in the coding round. Hand-wave on null safety or structured concurrency and you are out in the first loop.
If you came from the Android coding interview guide, you already know the bar. This post is the Kotlin language companion — the questions about the compiler itself.
Null safety — the NPE retirement plan
The first question is usually nullability. Not "what is null" — "why does your code compile at all."
In Kotlin, "the type system distinguishes between types that can hold null (nullable types) and those that cannot (non-nullable types). For example, a regular variable of type String cannot hold null." A String? can. A String cannot. The compiler enforces that distinction before the code runs.
You already know ?. (safe call), ?: (Elvis), and !! (not-null assertion). The interview question is when each one loses you the job.
The !! operator is the trap. Per the Kotlin docs: "if the value is null, the !! operator forces it to be treated as non-nullable, which results in an NPE." Every !! is a claim that you know better than the type system. It should survive the coding round only when nullability was already proved by a prior check the compiler cannot see.
Platform types are the subtler trap. "Any reference in Java may be null, which makes Kotlin's requirements of strict null-safety impractical for objects coming from Java." You will see them printed as String!. The compiler lets you treat them as non-null, but the call can fail at runtime. Treat any Java return as nullable until proved otherwise.
Coroutines — the cancellation cascade
The second question is concurrency. Not "what is a coroutine" — "what cancels when."
A suspending function is the primitive. Kotlin docs: "Kotlin's concept of suspending function provides a safer and less error-prone abstraction for asynchronous operations than futures and promises." A suspend fun can pause and resume without blocking a thread.
The interview story is structured concurrency. Per the docs: "Structured concurrency ensures that when a coroutine errors out or a user changes their mind and cancels the operation, all the work spawned by the coroutine is cancelled as well." A scope owns the coroutines launched inside it. Cancel the scope, everything it launched cancels with it.
On Android, the scope matters more than the dispatcher. The Android docs describe viewModelScope precisely: "Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared." Launch in viewModelScope, and the OS tearing down your ViewModel cancels every in-flight network call and every child coroutine. You do not have to write any of that logic.
// Scoped — cancels on ViewModel cleared.
viewModelScope.launch {
val feed = withContext(Dispatchers.IO) {
repo.loadFeed()
}
_state.value = FeedState.Loaded(feed)
}
// Unscoped — outlives the screen, leaks.
GlobalScope.launch {
val feed = repo.loadFeed()
}GlobalScope.launch is the anti-pattern. Nothing cancels it, so the request completes into a garbage-collected ViewModel and the work is wasted. The interview answer is "use viewModelScope or lifecycleScope, never GlobalScope."
Flow, StateFlow, SharedFlow, Channel
The third question is reactive state. There are four primitives, and the interviewer wants to know you can pick the right one.
Flow is the cold stream. "Flows are cold streams similar to sequences — the code inside a flow builder does not run until the flow is collected." Every collect restarts the pipeline. Repository queries emit Flow.
StateFlow is the hot state holder. Android docs: "StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors." It always has a value; new collectors get it immediately. Your ViewModel exposes StateFlow<FeedState>, your Compose layer collects it.
SharedFlow is the hot event stream. "SharedFlow is a hot flow that is highly configurable. It can be used to emit values to an arbitrary number of subscribers, and it can be configured to replay previously emitted values." Use it for one-shot events — navigation, snackbars — where a new collector should not see a replay.
Channel is the suspending queue. "A Channel is conceptually very similar to BlockingQueue. One key difference is that instead of a blocking put operation it has a suspending send, and instead of a blocking take operation it has a suspending receive." Prefer Flow for public API; use Channel for point-to-point handoff between coroutines.
Cold (Flow)
Hot (StateFlow / SharedFlow / Channel)
When does emission start
Per collector, on `collect` call
Independently of collectors
Default replay to new collector
Re-runs the flow from scratch
StateFlow: current value · SharedFlow: configurable · Channel: none
Use case
Repository query · data stream
StateFlow: UI state · SharedFlow: events · Channel: coroutine handoff
LiveData comparison
No direct analog
StateFlow is the coroutine-native replacement
The follow-up is LiveData. Android docs: "Unlike LiveData, StateFlow does not have automatic lifecycle awareness." StateFlow wins on coroutines interop; you get lifecycle awareness back through repeatOnLifecycle(Lifecycle.State.STARTED) { stateFlow.collect { } }, the idiomatic UI collection pattern in 2026.
Sealed classes — the state safety net
The fourth question is sealed types. Not "what is a sealed class" — "why does the compiler force you to handle every case."
A sealed hierarchy is closed — every direct subclass must live in the same package, so the compiler sees the full set of possibilities. The payoff is when. Per the docs, a when expression on a sealed class "allows the Kotlin compiler to check exhaustively that all possible cases are covered." An else branch is the smell. Add a new subclass and every exhaustive when in the codebase fails to compile until you handle it. That is the state-machine safety net.
enum class looks similar but is not. Per the docs: "Unlike enum classes where each constant is a single instance, sealed class subclasses can have multiple instances with different data." FeedState.Loading carries nothing. FeedState.Loaded(items: List<Item>) carries data. An enum cannot do that.
enum class
sealed class / sealed interface
Each case is
A single instance (constant)
A type — can carry its own fields
Exhaustive `when` check
Yes
Yes — compiler sees closed hierarchy
Inheritance
Cannot be subclassed
Sealed interface allows multiple supertypes
Use case
Fixed set of named constants
State machines · Result types · UI states
Sealed interfaces extend the story. Per the docs: "Direct subclasses of sealed classes and interfaces must be declared in the same package." A sealed interface can be implemented alongside other interfaces — the right tool when a Result needs to be both Retryable and Cacheable. Sealed classes give you state; sealed interfaces give you state plus composition.
Inline and reified — the erasure escape
The fifth question is inline. Not "what does inline do" — "why do you need reified."
Kotlin docs: "Inline functions eliminate the object allocation overhead of passing lambdas by copying the function body to the call site." Call an inline function, the compiler substitutes the body at the call site and the lambda allocation disappears. That is the performance story.
The interview story is erasure. JVM generics are erased at runtime — List<String> and List<Int> are the same type once the code runs. You cannot write x is T inside a normal generic function because T does not exist at runtime. Per the docs: "Reified type parameters allow you to access the actual type argument at runtime inside an inline function, bypassing JVM type erasure." Because the function is inlined, T is substituted for the actual type at the call site, and x is T compiles to the concrete check.
// Reified: T survives at runtime via inlining.
inline fun <reified T> isInstance(x: Any) = x is T
// Without reified, `x is T` won't compile:
// T is erased at runtime without inlining.The stdlib's filterIsInstance<String>() on a List<Any> returns a List<String> precisely because it is declared inline fun <reified T>. crossinline and noinline are the escape hatches — crossinline forbids non-local returns from a lambda, noinline opts a single lambda out of inlining. Name the three modifiers and the inline question is over.
If you came from Android, this is the language-level companion — the Kotlin questions your Android coding round will also probe. If you own iOS too, the cross-language pair is the Swift interview guide — Swift pushes defensiveness into the type system the same way Kotlin does, and mobile shops expect you to see the pattern in both languages. Mobile behavioral is the next post in this track.
The theme across all five questions is the same. Kotlin's distinctive features — nullable types, structured concurrency, cold-and-hot flows, sealed hierarchies, reified generics — all push runtime defense into compile-time proof. Null pointers become type errors. Async leaks become scope-cancellation guarantees. Non-exhaustive state handling becomes when compile failures. Erased generics become inlined concrete types. The interview is checking whether you can narrate that story one question at a time. If you can, the coding round is yours.
Fin and Coco are StrongYes editorial personas from the Council of Ternary Vertices — a trinary-star animal civilization that studies Earth's coding-interview process. Anecdotes map animal-universe experience to human interview mechanics; they are NEVER human-career claims. External citations link to public primary sources.
Grounded in public primary sources: the official Kotlin documentation at kotlinlang.org (null safety, coroutines, flow, sealed classes, inline functions, Java interop, channels), Android Developers documentation (coroutines architecture, StateFlow and SharedFlow), and the Kotlin Evolution and Enhancement Process (KEEP) proposals at github.com/Kotlin/KEEP. No login-walled citations.
Last verified Apr 16, 2026.
Practice Kotlin.
Reading builds recognition. Explaining builds recall. Run these problems with Fin or Coco.