Skip to main content

Kotlin engineer role-specific interview questions

30 questions on role-specific for kotlin engineer candidates. Expect prompts such as “Structured concurrency and scope cancellation propagation” and “Cold vs hot flows and when to use each”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 25 of 30 role-specific questions (page 1 of 2).

As asked

Explain structured concurrency in Kotlin. If a coroutine inside a CoroutineScope throws an exception, what exactly happens to its sibling coroutines and the parent scope? How does CancellationException differ from other exceptions in this propagation model?

Sample answer outline

The answer should cover how a child failure cancels the parent scope and all sibling coroutines unless SupervisorScope or SupervisorJob is used. CancellationException is treated specially, it cancels but does not propagate as a failure to the parent. The candidate should distinguish between Job and SupervisorJob and know that launch and async handle exceptions differently.

Expect these follow-ups

  • When would you use supervisorScope vs CoroutineScope?
  • If async throws, when exactly does the exception surface?
structured-concurrencycoroutinesexceptionskotlinscope

As asked

What is the fundamental difference between a cold Flow and a hot Flow like StateFlow or SharedFlow in Kotlin? Give me a concrete scenario where using a cold Flow would silently cause a bug if you mistakenly used a SharedFlow instead, or vice versa.

Sample answer outline

Cold flows restart their producer for each collector, so a network call in a cold flow fires once per subscriber. SharedFlow has a configurable replay cache and does not restart the producer. A common bug is using a StateFlow for one-shot events: late subscribers get the last cached value and replay side effects like navigation. The answer should mention that SharedFlow with replay=0 is safer for events.

Expect these follow-ups

  • How do you implement a one-time event stream safely using SharedFlow?
  • What does the buffer operator do to a cold flow and how does it interact with backpressure?
flowstateflowsharedflowkotlinreactive

As asked

You have an Android ViewModel that exposes a StateFlow. How do you collect it safely from a Fragment to avoid collecting while the Fragment is in the background? Compare repeatOnLifecycle, flowWithLifecycle, and launchWhenStarted and explain which one you prefer and why.

Sample answer outline

launchWhenStarted is deprecated because the coroutine is suspended but not cancelled, leaking resources. flowWithLifecycle is a Flow operator that wraps the source with lifecycle awareness. repeatOnLifecycle is the recommended approach: it cancels and restarts the collection block based on the lifecycle state, preventing background processing. The candidate should know to call it inside viewLifecycleOwner.lifecycleScope.launch.

Expect these follow-ups

  • What happens if the screen rotates while a StateFlow is being collected?
  • Why should you not use lifecycleScope.launch directly without repeatOnLifecycle?
androidviewmodelstateflowlifecyclekotlin

As asked

In an Android app using Room, you declare a DAO function that returns Flow<List<Item>>. When does the query run, how does Room emit updates reactively, and what is the risk of collecting this Flow on the main thread?

Sample answer outline

Room wraps the query in a cold Flow that runs on Dispatchers.IO by default and re-emits whenever the underlying table changes. Collection on the main thread is safe because Room dispatches the query internally, but the candidate should note that heavy data transformation on the collector should be done with map {} and flowOn(Dispatchers.Default). They should also mention distinctUntilChanged() to avoid redundant recompositions.

Expect these follow-ups

  • How does Room know when to re-emit after a table update?
  • What happens if two tables are joined in the query and only one of them changes?
roomflowandroidkotlindatabases

As asked

I ask candidates this frequently because I have seen it misused. Explain when you would reach for let vs run vs apply and what the semantic difference is between them. Give me a concrete case where using the wrong scope function introduces a subtle bug.

Sample answer outline

let transforms a value (returns lambda result, uses it), apply configures and returns the receiver, run is a combination (executes block with this, returns lambda result), also performs side effects and returns the receiver, with operates on a non-null receiver. A subtle bug: using apply on a nullable receiver without safe-call leads to NPE; using let where apply was intended returns Unit instead of the configured object, causing a type error or silent discard.

Expect these follow-ups

  • What does also return compared to let?
  • When would you use with instead of run?
scope-functionskotlinidiomsnull-safetyreadability

As asked

Walk me through how Kotlin coroutine dispatchers work under the hood. What actually happens when you call withContext(Dispatchers.IO) and how does the coroutine resume on the correct thread after the suspended call finishes?

Sample answer outline

A strong answer explains that dispatchers implement CoroutineDispatcher and schedule continuations onto thread pools. Dispatchers.IO uses a shared pool whose size is max(64, number of CPU cores) by default, and suspends by capturing the Continuation, then resumes it via dispatch() which posts the continuation to the pool. The candidate should mention the event loop for Dispatchers.Main on Android and how thread confinement is achieved without blocking.

Expect these follow-ups

  • What is the difference between Dispatchers.Default and Dispatchers.IO at the thread pool level?
  • What happens when you nest withContext(Dispatchers.IO) inside another withContext(Dispatchers.IO)?
coroutinesdispatchersconcurrencykotlinjvm

As asked

When you call a Java method from Kotlin, the return type becomes a platform type. What is a platform type, what notation does the IDE use to show it, and what are the real risks of assigning a platform type to a non-nullable Kotlin variable?

Sample answer outline

Platform types (shown as T! in IDE tooltips) have relaxed nullability, so the compiler does not enforce null checks. Assigning a platform type to a non-nullable var passes compilation but can throw NPE at runtime if the Java side returns null. The fix is to explicitly annotate Java APIs with @Nullable/@NonNull or wrap the call at the boundary. The candidate should also know about @JvmField and the role of JSR-305 annotations.

Expect these follow-ups

  • How do you enforce null safety at a Kotlin/Java interop boundary in a library you don't control?
  • What is the difference between !! and the safe-call operator in terms of what bytecode they generate?
null-safetyjava-interopplatform-typeskotlinjvm

As asked

When would you choose a sealed class hierarchy over an enum class in Kotlin? Explain how the compiler enforces exhaustiveness in a when expression for each, and describe a real modeling situation where sealed classes are clearly superior.

Sample answer outline

Sealed classes allow each subtype to carry different data, which enums cannot do. The compiler enforces exhaustive when only when used as an expression, not a statement. A strong answer uses a UI state model: Loading, Success(data: T), Error(cause: Throwable) where each state carries distinct fields. The candidate should also mention sealed interfaces introduced in Kotlin 1.5 and how they allow implementing across files in the same module.

Expect these follow-ups

  • Can a sealed class have non-sealed subclasses?
  • How does exhaustiveness work with sealed interfaces vs sealed classes in Kotlin 1.5+?
sealed-classesenumstype-systemkotlinwhen-expression

As asked

Explain what the inline keyword does to a Kotlin function at the bytecode level. Then explain why reified type parameters are only possible on inline functions, and give an example of where reified saves you from passing a Class<T> explicitly.

Sample answer outline

Inline functions have their body copied to every call site by the compiler, eliminating the lambda object allocation and enabling non-local returns. Reified works because after inlining, the concrete type is known at the call site, so the compiler substitutes it directly, bypassing JVM type erasure. A canonical example is an inline fun <reified T> Gson.fromJson(json: String): T that calls fromJson(json, T::class.java) internally.

Expect these follow-ups

  • What are the drawbacks of inlining large functions?
  • Can you use noinline and crossinline together in the same function, and what do they each do?
inlinereifiedgenericskotlinjvm-bytecode

As asked

How does the by lazy delegate work at the bytecode level in Kotlin? What is the default thread-safety mode, what are the other modes, and in what situation would you switch away from the default?

Sample answer outline

by lazy generates a backing field of type Lazy<T> and delegates getValue() to it. The default mode is LazyThreadSafetyMode.SYNCHRONIZED, which uses double-checked locking. PUBLICATION allows multiple threads to compute but uses the first result. NONE has no synchronization and is suitable for single-threaded contexts like Android main-thread-only properties, offering lower overhead.

Expect these follow-ups

  • How does by lazy differ from lateinit var in terms of use cases and failure modes?
  • Can you implement a custom property delegate that validates the value on assignment?
delegationlazythread-safetykotlinproperties

As asked

When you write a Kotlin library that Java code will consume, what annotations and design choices do you use to make the API feel natural in Java? Cover at least three specific scenarios.

Sample answer outline

Strong answers mention @JvmStatic on companion object methods to avoid the Java Companion.INSTANCE indirection, @JvmOverloads to expose default-parameter overloads, @JvmField to expose properties as fields rather than getters, and @Throws to declare checked exceptions. The candidate should also know that extension functions appear as static methods with the receiver as the first argument.

Expect these follow-ups

  • What does @JvmName do and when is it necessary?
  • How do top-level functions in a Kotlin file look from Java, and how do you rename the generated class?
java-interopjvm-annotationskotlinlibrary-designinterop

As asked

You have a Kotlin data class with a mutable list as one of its properties. A junior engineer copies it with .copy() and expects the result to be fully independent. Explain what actually happens and how you would design the class to avoid this issue.

Sample answer outline

copy() does a shallow copy, so the new instance shares the same list reference. Mutations on either object affect both. The fix is to use an immutable list (List<T> backed by listOf) or to explicitly deep-copy mutable fields in a custom copy constructor. The candidate should also note that data classes should ideally contain only immutable properties and that using MutableList in a data class is a design smell.

Expect these follow-ups

  • How does Kotlin's structural equality (==) work for data classes with mutable list properties?
  • Would this issue affect equals() and hashCode() behavior?
data-classimmutabilitykotlincopystate-management

As asked

How do you test a suspend function that involves delays and a StateFlow that emits multiple values over time? Walk me through the setup, the assertions you would write, and what problems arise if you use the real Dispatchers.IO in a unit test.

Sample answer outline

The answer should cover replacing Dispatchers with StandardTestDispatcher or UnconfinedTestDispatcher from kotlinx-coroutines-test (TestCoroutineDispatcher was deprecated in 1.6 and removed in 1.7), using advanceTimeBy() to skip delays, and using runTest as the test runner. For Flow testing, Turbine's awaitItem() and cancelAndConsumeRemainingEvents() give clean assertions. Using real Dispatchers.IO makes tests flaky and slow due to real thread creation and timing.

Expect these follow-ups

  • What is the difference between StandardTestDispatcher and UnconfinedTestDispatcher?
  • How do you inject the dispatcher into your ViewModel or UseCase for testability?
testingcoroutinesflowkotlinturbine

As asked

Walk me through setting up JWT authentication in a Ktor server application. Where does the authentication configuration live, how do you protect specific routes, and how do you extract claims from the token inside a route handler?

Sample answer outline

Authentication is installed via install(Authentication) with jwt() configured with a verifier and a validate block that returns a JWTPrincipal or null. Routes are protected by wrapping them in authenticate('jwt') blocks. Claims are extracted via call.principal<JWTPrincipal>()?.payload?.getClaim(). A strong answer notes the difference between session and JWT auth and mentions refresh token handling.

Expect these follow-ups

  • How do you handle token expiry and refresh in Ktor without blocking the main thread?
  • How does Ktor's plugin system differ from Spring's interceptor chain?
ktorjwtauthenticationbackendkotlin

As asked

If a class has a member function and you define an extension function with exactly the same signature, which one wins and why? When are extension functions useful despite this limitation?

Sample answer outline

Member functions always win over extension functions with the same signature. Extensions are statically dispatched at compile time based on the declared type, not the runtime type. They are still useful for adding utility to classes you don't own, for scoping helper functions to a type without polluting the global namespace, and for building DSLs. A strong answer also notes that extension functions do not support polymorphism in the traditional sense.

Expect these follow-ups

  • Can extension functions access private members of a class?
  • How does extension function resolution interact with nullable receivers?
extension-functionskotlinoopdispatchlanguage-design

As asked

What is the difference between an object declaration and a companion object in Kotlin? Describe the initialization guarantee for object declarations and when that guarantee could cause subtle bugs in an Android app.

Sample answer outline

Object declarations are singletons initialized lazily on first access, thread-safe by the JVM class loading mechanism. Companion objects are like a static nested class tied to the enclosing class. A subtle Android bug is accessing an object declaration that holds a Context reference: if the object is initialized before the Application is fully set up or holds a stale context after process death, you get memory leaks or incorrect behavior.

Expect these follow-ups

  • Can an object declaration implement an interface?
  • How does Kotlin's object compare to a Java static singleton pattern in terms of testability?
object-declarationcompanion-objectsingletonkotlinandroid

As asked

How does Spring Boot support Kotlin coroutines in REST controllers? What happens at the framework level when you declare a controller function as a suspend fun, and how does Spring handle the underlying reactive or thread-pool model to support it?

Sample answer outline

Spring WebFlux with the Kotlin coroutines integration adapts suspend functions via a CoroutineHttpHandlerAdapter that wraps the function in a Mono and subscribes on the Reactor event loop. Spring MVC with the coroutines support (spring-webmvc-coroutines in Spring 6) dispatches the suspend function using a coroutine launched on the request thread pool, not on Project Loom virtual threads by default. Project Loom virtual thread support in Spring 6 is a separate feature enabled via the applicationTaskExecutor and does not automatically apply to suspend functions. The candidate should understand that suspend fun in Spring is not truly non-blocking unless the underlying IO uses coroutine-compatible async drivers.

Expect these follow-ups

  • What database drivers support Kotlin coroutines natively for use with Spring?
  • How do you propagate Spring's SecurityContext across coroutines?
spring-bootcoroutinesbackendkotlinwebflux

As asked

Describe a realistic scenario in an Android app where an incorrectly scoped coroutine causes a memory leak. What tools would you use to detect it and what would you change in the code?

Sample answer outline

A classic leak is launching a coroutine in GlobalScope from an Activity: the coroutine holds a reference to the Activity's View or ViewModel long after the Activity is destroyed, preventing GC. Detection uses LeakCanary or Android Studio's Memory Profiler. The fix is using viewModelScope or lifecycleScope, which are bound to their owners' lifecycle and cancel automatically on destruction.

Expect these follow-ups

  • What is the difference between viewModelScope and lifecycleScope in terms of when they are cancelled?
  • Can you leak memory using viewModelScope incorrectly?
memory-leaksandroidcoroutineskotlindebugging

As asked

You are publishing a Kotlin library that will be used heavily from Java. What do you change in the API design?

Sample answer outline

The public API should account for Java callers who do not see Kotlin conveniences the same way. Use @JvmOverloads sparingly for default arguments, @JvmStatic or companion objects where Java ergonomics matter, and avoid exposing Kotlin-specific types such as Unit-heavy callbacks or sealed hierarchies without a Java-friendly shape. Nullability annotations must be accurate because they become the Java caller's main contract. Binary compatibility matters, so changing default parameters or data class constructor shape can be a breaking change. A strong answer balances idiomatic Kotlin internals with stable Java-facing boundaries.

Expect these follow-ups

  • How do data classes appear to Java callers?
  • When would you write a separate Java facade?
  • What API changes are binary compatible but source awkward?
jvmapi-designinterop

As asked

Explain the typeclass pattern in Scala and give a practical example where it improves an API without making the codebase harder to maintain.

Sample answer outline

A typeclass represents behaviour for a type without requiring inheritance or modifying the original type. In Scala this is usually encoded as a trait plus given or implicit instances and extension methods. It improves APIs when behaviour needs to be added across types from different modules, such as JSON encoding, ordering or database binding. The candidate should discuss discoverability and error messages because excessive implicit abstraction can hurt maintainability. Strong answers show restraint: use typeclasses at stable boundaries, not for every helper function.

Reference implementation (scala)

Scala
trait CsvEncoder[A] {
  def encode(value: A): List[String]
}

object CsvEncoder {
  given CsvEncoder[User] with {
    def encode(value: User): List[String] =
      List(value.id.toString, value.email)
  }
}

def writeCsv[A](rows: List[A])(using encoder: CsvEncoder[A]): List[String] =
  rows.map(row => encoder.encode(row).mkString(","))

Expect these follow-ups

  • How do given instances in Scala 3 improve or worsen implicit search?
  • When is inheritance simpler than a typeclass?
  • How do you debug an ambiguous implicit error?
typeclassesscala-3api-design

As asked

CoroutineContext is described as an indexed set of Element objects. How does element lookup work, how do you combine two contexts with the plus operator, and what happens to duplicate keys when you combine them?

Sample answer outline

CoroutineContext stores elements keyed by their companion object Key. The plus operator creates a combined context where the right-hand element overwrites any existing element with the same key. Lookup via context[Key] traverses the internal linked structure. The candidate should name common elements: Job, CoroutineName, CoroutineDispatcher, CoroutineExceptionHandler.

Expect these follow-ups

  • If you add a new CoroutineExceptionHandler to a child coroutine's context, does it override the parent handler for the child's exceptions?
  • How does coroutineContext propagate from parent to child?
coroutine-contextcoroutinesconcurrencykotlininternals

As asked

Walk me through what happens internally when you write flow.map { }.filter { }.collect { }. How does each operator create a new Flow, and how does back-pressure work when the collector is slower than the producer?

Sample answer outline

Each operator like map returns a new Flow whose collect block wraps the upstream collector. The chain is assembled lazily and execution only starts on terminal collect. Back-pressure is natural because collect is a suspend function, so the upstream producer suspends at each emit until the downstream processes it. The candidate should mention buffer() and conflate() as ways to decouple producer and consumer speeds.

Expect these follow-ups

  • What is the difference between conflate() and collectLatest()?
  • How do you share a cold flow among multiple collectors without restarting the producer each time?
flowoperatorsbackpressurekotlincoroutines

As asked

You launch a coroutine with a CoroutineExceptionHandler attached to the scope's context. Inside the coroutine, a child coroutine launched with launch throws an exception. Does the handler catch it? What if the child was launched with async and you forget to await it?

Sample answer outline

CoroutineExceptionHandler catches uncaught exceptions from root coroutines. A child launched with launch that throws propagates to the parent, cancels the scope, and the handler catches it. With async inside a non-supervisor scope, the exception propagates to the parent immediately when the child fails, regardless of whether await() is called. Only inside a supervisorScope does an async child store the exception in the Deferred and defer it until await(). If await() is never called in that case the exception is silently swallowed, which is a common bug. The handler does not catch async exceptions that are not awaited under supervisorScope.

Expect these follow-ups

  • How do you detect unhandled async exceptions globally in a Kotlin application?
  • What is the role of Job vs SupervisorJob here?
coroutinesexception-handlingasynckotlindebugging

As asked

Explain the difference between declaration-site variance (out T, in T on the class definition) and use-site variance (out T in a function parameter) in Kotlin. Give a concrete example showing when covariance with out would cause a compile error without it.

Sample answer outline

Declaration-site variance marks the entire class as covariant (out) or contravariant (in). out T means the class only produces T, so List<String> is a List<Any>. in T means the class only consumes T. Use-site variance is applied per function for classes that are neither purely producers nor consumers. A classic example: a function that reads from a Collection<out T> can accept Collection<String> where T is Any.

Expect these follow-ups

  • Why is MutableList invariant in Kotlin while List is covariant?
  • What is a star projection and when would you use Array<*> instead of Array<out Any>?
genericsvariancecovariancekotlintype-system

As asked

When would you use a Channel instead of a Flow for producer-consumer communication in Kotlin? Explain the differences in lifecycle, back-pressure, and fan-out behavior, and describe a case where Channel is clearly the better choice.

Sample answer outline

Channels are hot, stateful, and support multiple consumers (fan-out) sharing items without replay. Flow is cold and each collector gets all items independently. A good use case for Channel is a work queue where multiple coroutines compete for items: each item should be processed by exactly one worker. Channels provide explicit back-pressure via capacity (UNLIMITED, BUFFERED, RENDEZVOUS). The candidate should mention that Channels require explicit closing.

Expect these follow-ups

  • What is the difference between Channel.RENDEZVOUS and Channel.CONFLATED?
  • How does produce() builder relate to Channel?
channelflowconcurrencykotlincoroutines