Ryz Labs/Interview questions/Kotlin developer
Interview questions

Kotlin interview questions for senior developers (2026)

Language-level Kotlin questions for senior hires: null safety, coroutines and Flow, sealed types, generics, the K2 compiler, Multiplatform and Kotlin on the backend.

These Kotlin interview questions test the language and its concurrency model, whether the candidate writes Android apps, Spring Boot or Ktor services, or shared Kotlin Multiplatform code. They cover null safety and Java interop, sealed hierarchies, inline functions and reified generics, delegation, the K2 compiler, coroutines with structured concurrency, Flow, and design questions for libraries and backends. Platform topics such as Compose, WorkManager and Play releases are in our Android set. At Ryz, recruiters source senior Kotlin engineers from their production work, candidates complete structured NTRVSTA AI interviews on material like this, and recruiters review each one before and after. AI scores are advisory; people make the calls.

How to use these questions

Most Kotlin developers can write idiomatic code. Fewer understand what happens when a coroutine is cancelled mid-transaction or why a runCatching block broke cancellation. For senior roles, spend at least half the interview in the coroutines and Flow section.

Fundamentals

A Kotlin service throws a NullPointerException in code with no !! operator. How?

Platform types. Values from Java without nullability annotations arrive as String!, and Kotlin lets you treat them as non-null. If the Java method returns null, the failure appears where the value is used. Other sources: lateinit read before assignment (a different exception), and reflection-based deserializers that bypass constructors and leave non-null properties null.

What a strong answer shows: They know null safety ends at the Java and reflection boundaries and declare explicit types there.

Is a Kotlin List immutable?

No. List is a read-only interface. The underlying object may be an ArrayList that someone else mutates through a MutableList reference. For true immutability, use the kotlinx.collections.immutable library or defensive copies.

val backing = mutableListOf(1, 2, 3)
val view: List<Int> = backing
backing.add(4)
println(view) // [1, 2, 3, 4]

What a strong answer shows: They distinguish read-only views from immutable data, which matters for shared state.

What does a data class generate, and what are its pitfalls?

equals, hashCode, toString, copy and componentN from primary constructor properties only. Properties declared in the body are ignored by equality. A data class with a var used as a HashMap key breaks lookups after mutation. copy is shallow, and on JPA entities data classes cause trouble with lazy loading and identity.

What a strong answer shows: They know where data classes fit (values) and where they do not (entities).

When do you use a sealed interface instead of an enum?

Enums are fixed singletons with the same shape. Sealed hierarchies allow different data per case and still give exhaustive when expressions, so adding a case is a compile error everywhere it is not handled.

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

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

What a strong answer shows: They use exhaustiveness as a safety net and avoid else branches that hide new cases.

Are extension functions dispatched virtually?

No. They are resolved statically from the declared type, and a member function with the same signature always wins. An extension on Shape called on a Circle stored as Shape uses the Shape version.

What a strong answer shows: They understand extensions are syntax for static functions and do not expect overriding.

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

apply configures an object and returns it, also runs side effects and returns the object, let transforms (often with ?.), run and with compute a result using the object as receiver. The real rule is readability: nested scope functions with shadowed it are worse than a local variable.

What a strong answer shows: Restraint, not memorized tables.

What is a companion object, and how does it look from Java?

A singleton tied to the class, used for factories and constants. From Java, members are accessed through Companion unless annotated with @JvmStatic or @JvmField, and const val compiles to a static field.

What a strong answer shows: Awareness of how Kotlin APIs look to Java callers.

Intermediate

What do inline and reified do, and when are crossinline and noinline needed?

inline copies the function body and its lambda arguments to the call site, avoiding lambda allocations and allowing non-local return. Because the body is inlined, type parameters can be reified and checked at runtime. crossinline forbids non-local returns when the lambda is called from another context; noinline keeps a lambda as an object so it can be stored.

inline fun <reified T> Any?.asOrNull(): T? = this as? T

inline fun <reified T : Any> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)

What a strong answer shows: They know why reified needs inline and avoid inlining large functions for no gain.

How do lazy and class delegation work?

by lazy computes on first access; the default mode is synchronized, LazyThreadSafetyMode.PUBLICATION allows concurrent computation, and NONE is for single-threaded code. Class delegation (class LoggingRepo(inner: Repo) : Repo by inner) forwards interface methods to another object so you override only what you need, a clean decorator without inheritance.

What a strong answer shows: They pick the lazy mode deliberately and prefer composition over inheritance.

Explain in and out variance with an example.

out T means the type only produces T, so List<String> is a List<Any>. in T means it only consumes T, so a function of type (Any) -> Unit can be used where (String) -> Unit is expected. Kotlin declares variance at the declaration site, unlike Java wildcards at every use. Star projection (List<*>) is for when the type is unknown.

What a strong answer shows: They can design a generic API that accepts what callers naturally have.

What are value classes, and when do they box?

@JvmInline value class UserId(val raw: String) gives type safety with no wrapper at runtime in most cases. It boxes when used as a nullable type in some cases, as a generic type argument, or through an interface. Function names get mangled in bytecode, which affects Java callers.

What a strong answer shows: They use value classes for domain IDs and know the boxing and interop costs.

When does a Sequence beat a List chain?

List operations are eager and create an intermediate list per step. Sequences are lazy and process one element through the whole chain, which wins on large inputs with early termination (first, take). For small collections, the list version is often faster.

What a strong answer shows: They benchmark rather than assume sequences are always faster.

What changed with the K2 compiler, and what breaks when upgrading to Kotlin 2.x?

Kotlin 2.0 made K2 the default: a new frontend with faster compilation and smarter smart casts (for example across local variables and after || checks). Upgrades break on compiler plugins that were not updated, kapt (whose K2 support lagged, so teams move to KSP), and code relying on old inference quirks. Compose's compiler plugin now ships with Kotlin itself.

What a strong answer shows: Hands-on upgrade experience, including the plugin and annotation-processing side.

Coroutines and Flow

What is structured concurrency, and how do coroutineScope and supervisorScope differ?

Every coroutine has a parent scope; the parent waits for children and cancellation propagates down. In coroutineScope, one failing child cancels its siblings and rethrows. In supervisorScope, children fail independently. async holds its exception until await; launch propagates it to the parent.

suspend fun dashboard(userId: String): Dashboard = coroutineScope {
val profile = async { api.profile(userId) }
val orders = async { api.orders(userId) }
Dashboard(profile.await(), orders.await()) // one failure cancels the other
}

What a strong answer shows: They choose failure semantics on purpose and never reach for GlobalScope.

Why is this error handling wrong?

suspend fun refresh() {
val result = runCatching { repository.sync() }
result.onFailure { log.warn("sync failed", it) }
}

runCatching also catches CancellationException. If the caller is cancelled, the coroutine logs a warning and keeps going instead of stopping. Catch specific exceptions, or rethrow CancellationException before handling others.

What a strong answer shows: They know cancellation is an exception and treat swallowing it as a bug.

When do you use Dispatchers.IO versus Dispatchers.Default, and how do you limit parallelism?

Default is sized to CPU cores for computation. IO allows many more threads for blocking calls such as JDBC or file access. Wrap blocking calls in withContext(Dispatchers.IO). To cap concurrency against a fragile dependency, use Dispatchers.IO.limitedParallelism(n) or a Semaphore.

What a strong answer shows: They never block Default or the main thread, and they bound load on downstream systems.

Cold Flow, StateFlow and SharedFlow: when do you use each?

A cold Flow runs its producer per collector. StateFlow always has a current value, conflates and skips equal values, and suits UI state. SharedFlow broadcasts events with configurable replay and buffer. Convert cold to hot with stateIn or shareIn.

val uiState: StateFlow<UiState> = repository.observeOrders()
.map { UiState.Loaded(it) }
.stateIn(scope, SharingStarted.WhileSubscribed(5_000), UiState.Loading)

What a strong answer shows: They understand conflation and why WhileSubscribed with a timeout avoids restarting work on brief unsubscribes.

Implement search-as-you-type that cancels stale requests.

Debounce the query, skip duplicates, and use flatMapLatest (or mapLatest) so a new query cancels the in-flight request.

queries
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { q -> if (q.isBlank()) flowOf(emptyList()) else flow { emit(api.search(q)) } }
.catch { emit(emptyList()) }

What a strong answer shows: Correct operator choice and awareness that catch placement ends the flow on error unless handled inside the inner flow.

How do you test coroutine code without real delays?

Use runTest from kotlinx-coroutines-test, which runs on virtual time so delay is skipped. Inject dispatchers rather than hardcoding them, and use StandardTestDispatcher to control execution order. Turbine makes Flow assertions readable.

What a strong answer shows: Injected dispatchers and deterministic tests.

Why is synchronized a problem in coroutines, and what do you use instead?

A suspending call inside a synchronized block is not allowed by the compiler, and blocking locks waste threads that other coroutines need. Use Mutex.withLock, which suspends instead of blocking, confine state to a single-threaded dispatcher or an actor-like channel, or use atomics for simple counters.

What a strong answer shows: They know the difference between blocking and suspending exclusion.

Senior and architecture

What would you share in a Kotlin Multiplatform project, and what would you keep native?

Share domain logic, networking (Ktor client), persistence (SQLDelight or Room's multiplatform support), and validation. Keep platform UI native unless the team chooses Compose Multiplatform, whose iOS support is now stable. Use expect/actual or interfaces for platform APIs. Plan the Swift-facing API carefully: suspend functions and Flows need wrappers or tooling to feel natural in Swift.

What a strong answer shows: They think about the iOS developer's experience, not just code reuse percentages.

What do you need to know to use Kotlin with Spring Boot?

Kotlin classes are final, so the kotlin-spring (all-open) plugin opens classes Spring proxies; kotlin-jpa generates no-arg constructors for entities. Spring supports suspend controller methods and Flow in WebFlux, and in MVC with virtual threads plain blocking code may be simpler. Avoid data classes for JPA entities.

What a strong answer shows: They know the compiler plugins and the blocking versus reactive trade-off.

Ktor or Spring Boot for a new service?

Ktor is lightweight, coroutine-native and explicit, good for small services and teams that want control. Spring Boot brings a large ecosystem (security, data, observability) and conventions that help large organizations. Choose by team experience, ecosystem needs and operations tooling, not benchmarks alone.

What a strong answer shows: A context-based decision instead of framework loyalty.

How do you design a Kotlin library that Java teams also call?

Use @JvmOverloads for default arguments, @JvmStatic and @JvmName where needed, avoid exposing suspend functions alone (offer a CompletableFuture variant), and avoid value classes in public signatures. Turn on explicit API mode and the binary compatibility validator to catch accidental breaking changes.

What a strong answer shows: Respect for callers outside Kotlin and tooling that prevents ABI breaks.

How would you build a type-safe DSL in Kotlin?

Lambdas with receivers let callers configure a builder in a block. @DslMarker stops inner blocks from silently calling outer receivers.

@DslMarker annotation class HtmlDsl

@HtmlDsl class Ul { val items = mutableListOf<String>(); fun li(text: String) { items += text } }
@HtmlDsl class Page { val lists = mutableListOf<Ul>(); fun ul(block: Ul.() -> Unit) { lists += Ul().apply(block) } }

fun page(block: Page.() -> Unit) = Page().apply(block)

val p = page { ul { li("One"); li("Two") } }

What a strong answer shows: They understand receivers and scope control, and know DSLs can hurt readability if overused.

Exceptions, Result or sealed results: how do you model errors across layers?

Use exceptions for unexpected failures and sealed result types for expected business outcomes (declined payment, not found) that callers must handle. Avoid kotlin.Result in public APIs for domain errors because its error type is just Throwable. Libraries like Arrow offer typed errors for teams that want a functional style.

What a strong answer shows: A consistent policy that makes expected failures visible in types.

How do you migrate a large Java codebase to Kotlin?

Write new code in Kotlin, convert files when you touch them, starting with tests and leaf classes. Add nullability annotations to Java code first so Kotlin sees real types. Agree on style and lint rules (ktlint or detekt), and watch build time from mixed compilation.

What a strong answer shows: Incremental change with interop hygiene, not a big-bang rewrite.

A coroutine-based service leaks memory and threads under load. Where do you look?

Scopes that are never cancelled (custom CoroutineScope objects created per request), GlobalScope launches, unbounded channels or SharedFlow buffers, and blocking calls on Default causing thread starvation. Use coroutine debug probes and thread dumps to find stuck coroutines, and tie every scope to a lifecycle.

What a strong answer shows: Production debugging skill and the habit of owning scope lifetimes.

Red flags to watch for

A practical exercise

Give a 3-hour take-home: a small Ktor or Spring Boot service, or a pure Kotlin module, that aggregates prices from three slow upstream APIs. Each request must call all three concurrently, return partial results if one times out after 800 ms, cap concurrent calls per upstream, and cache results for 30 seconds. Provide fake upstreams with configurable latency and failures. Follow up with a 30-minute walkthrough.

Hire senior Kotlin developers vetted with these questions

You can also hire senior Kotlin developers through Ryz, on Android, Multiplatform or the backend. They are the top 1% of the candidates we interview, they work on your team and in your repos, and they work within ±1h of US time zones. Read how screening works in our vetting process, and use our Kotlin developer job description to scope the role.

FAQ

Do Kotlin backend and Android roles need different interviews?

The language and coroutine questions overlap. Swap the platform part: Android roles need lifecycle and Compose questions, backend roles need Spring or Ktor, database transactions and service design. Do not reject a backend candidate for not knowing Android APIs.

How deep should coroutine questions go for mid-level roles?

Mid-level engineers should explain scopes, dispatchers and cancellation basics and write correct viewModelScope or request-scoped code. Senior engineers should also reason about exception propagation, supervisor scopes, Flow sharing and testing with virtual time.

Is a take-home or live coding better for Kotlin?

For concurrency, a take-home with tests is fairer: realistic coroutine code needs time to get right. Follow it with a short live extension, such as adding a timeout or a new upstream, to confirm they wrote it and can change it.

Questions we didn't answer? Email info@ryzlabs.com.

Explore Ryz Labs

Staff augmentationDedicated development teamsAI pod teamsForward deployed engineersNearshore software developmentAI engineering teamsHire engineers by roleRyz Labs vs competitorsAlternatives guidesBuyer guidesCase studiesHow we vet engineers
Ryz Labs

Senior engineers in your time zone. AI pod teams that ship.

Tell us who you need. You'll get a scoped plan, a price and the names of the people who would do the work.

Start a conversation →