Skip to main content

Kotlin engineer coding interview questions

12 questions on coding for kotlin engineer candidates. Expect prompts such as “Reverse a singly linked list” and “Two Sum using a HashMap”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 12 of 12 coding questions.

As asked

Given a singly linked list defined as a sealed class hierarchy in Kotlin, write a function that reverses it iteratively and returns the new head. Do not allocate any new nodes.

Sample answer outline

The iterative solution uses three pointers: prev, current, and next. It traverses the list, reversing each next pointer in place. A strong answer recognizes that the sealed class structure allows exhaustive pattern matching for empty-list handling and uses Kotlin idioms like apply or let for clarity.

Reference implementation (kotlin)

Kotlin
sealed class Node
object Empty : Node()
data class Cons(val value: Int, var next: Node) : Node()

fun reverse(head: Node): Node {
    // implement here
}

Expect these follow-ups

  • How would you reverse it recursively in Kotlin and what is the stack depth risk?
  • How would you write a test for this function using JUnit and asserting structural equality?
linked-listdata-structureskotlinalgorithmsin-place

As asked

Write a Kotlin function that takes an IntArray and a target Int and returns the indices of the two numbers that add up to the target. Assume exactly one solution exists. Solve it in O(n) time using a map and use idiomatic Kotlin.

Sample answer outline

The solution maintains a HashMap of value-to-index seen so far. For each element, compute the complement and check the map. Return immediately when found. Idiomatic Kotlin uses forEachIndexed and avoids null-unsafe map lookups by checking containsKey before get, or using getOrDefault.

Reference implementation (kotlin)

Kotlin
fun twoSum(nums: IntArray, target: Int): IntArray {
    val seen = mutableMapOf<Int, Int>()
    nums.forEachIndexed { i, num ->
        val complement = target - num
        // complete the implementation
    }
    throw IllegalArgumentException("No solution")
}

Expect these follow-ups

  • How would you change this to return all pairs if multiple solutions existed?
  • What happens if the array contains duplicates?
hash-mapalgorithmskotlintwo-sumarrays

As asked

Write a suspend function in Kotlin that takes a list of items and a suspend lambda, applies the lambda to each item in parallel using coroutines, and returns a list of results preserving the original order. Do not use GlobalScope.

Sample answer outline

The solution uses coroutineScope to ensure structured concurrency. It maps each item to an async Deferred and then calls awaitAll() on the resulting list, which preserves order. The candidate should know that awaitAll() is preferred over map { it.await() } because it cancels all other Deferreds if one fails.

Reference implementation (kotlin)

Kotlin
suspend fun <T, R> List<T>.parallelMap(
    transform: suspend (T) -> R
): List<R> = coroutineScope {
    // implement here
}

Expect these follow-ups

  • How would you limit concurrency to at most N parallel coroutines?
  • What changes if you need to return partial results even when some items fail?
coroutinesparallelasynckotlinconcurrency

As asked

Design a Go API client method that respects request deadlines, cancellation and cleanup. What mistakes do you avoid?

Sample answer outline

The method should accept context.Context as the first parameter and pass it into network, database or RPC calls rather than inventing a separate timeout mechanism. It should not store context in a struct for long-lived reuse, and it should always call cancel for derived contexts to release timers. Cleanup should happen even on cancellation, but expensive retries should stop when ctx.Done is closed. Strong candidates mention distinguishing context.Canceled from context.DeadlineExceeded in logs and metrics. The common failure is to create a timeout but then call an API that ignores it.

Reference implementation (go)

Go
func FetchUser(ctx context.Context, client *http.Client, baseURL, id string) (*User, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/"+id, nil)
	if err != nil {
		return nil, err
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("fetch user: status %d", resp.StatusCode)
	}

	var user User
	if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
		return nil, err
	}
	return &user, nil
}

Expect these follow-ups

  • When should a library create a child timeout versus trusting the caller's deadline?
  • How do you test cancellation deterministically?
  • What should be logged when the client disconnects?
contexthttpcancellation

As asked

Write a Kotlin function that wraps a suspend lambda producing a value inside a Flow, retries on IOException up to maxRetries times with exponential backoff starting at initialDelayMs, and propagates other exceptions immediately. Return the Flow.

Sample answer outline

The solution builds a flow { emit(producer()) } and applies retryWhen { cause, attempt -> }. Inside retryWhen, check if cause is IOException and attempt < maxRetries, then delay the backoff using 2.0.pow(attempt.toDouble()).toLong() multiplied by initialDelayMs, and return true to retry. For other exceptions or exceeded retries, return false or rethrow. Note that 2.pow() is not a direct Kotlin Int method, so use Math.pow or the kotlin.math.pow extension on Double.

Reference implementation (kotlin)

Kotlin
fun <T> withRetry(
    maxRetries: Int,
    initialDelayMs: Long,
    producer: suspend () -> T
): Flow<T> = flow {
    emit(producer())
}.retryWhen { cause, attempt ->
    // implement here
}

Expect these follow-ups

  • How do you add jitter to the backoff to avoid thundering herd?
  • How would you write a unit test for this without real IO or real delays?
flowretrybackoffkotlinerror-handling

As asked

Implement an LRU cache in Kotlin that supports get(key) in O(1) and put(key, value) in O(1). Use only standard Kotlin/Java collections. Explain why the data structure you chose works.

Sample answer outline

The canonical solution uses LinkedHashMap with accessOrder=true, which reorders entries on access. Override removeEldestEntry to evict when the size exceeds capacity. The candidate should know that LinkedHashMap maintains O(1) access via HashMap and O(1) eviction via its doubly-linked list. A strong answer wraps the get/put in synchronized blocks or mentions thread-safety considerations.

Reference implementation (kotlin)

Kotlin
class LRUCache<K, V>(private val capacity: Int) {
    private val cache = object : LinkedHashMap<K, V>(capacity, 0.75f, true) {
        override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?) =
            size > capacity
    }

    fun get(key: K): V? = cache[key]
    fun put(key: K, value: V) { cache[key] = value }
}

Expect these follow-ups

  • How would you make this cache thread-safe for concurrent access?
  • How would you extend it to support a TTL per entry?
data-structureslru-cachekotlinalgorithmscollections

As asked

Write a generic binary search function in Kotlin that works on any List<T> where T is Comparable<T>. Return the index of the target or -1 if not found. Explain any edge cases.

Sample answer outline

The function uses in T : Comparable<T> upper bound. The loop maintains lo and hi indices, computes mid = lo + (hi - lo) / 2 to avoid overflow (even though Kotlin Int wraps rather than overflows), and compares target.compareTo(list[mid]). Edge cases include empty list, single element, and target not present. The candidate should avoid mid = (lo + hi) / 2 for clarity.

Reference implementation (kotlin)

Kotlin
fun <T : Comparable<T>> binarySearch(list: List<T>, target: T): Int {
    var lo = 0
    var hi = list.size - 1
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        // implement comparison and adjustment
    }
    return -1
}

Expect these follow-ups

  • How would you adapt this to find the leftmost occurrence of a duplicate target?
  • What changes if the list is not sorted and you still need to find an element?
binary-searchalgorithmskotlingenericssorted-list

As asked

Given a list of IntRange objects in Kotlin representing intervals, write a function that merges all overlapping intervals and returns a new sorted list. For example, [(1..3), (2..6), (8..10), (15..18)] should return [(1..6), (8..10), (15..18)].

Sample answer outline

Sort the list by first. Iterate through sorted ranges, comparing each range's first to the last merged range's last. If the current range starts before or at the end of the last merged range, extend the merged range's last to max of the two lasts. Otherwise add a new entry. The Kotlin solution can use sortedBy { it.first } and a mutableListOf for the result.

Reference implementation (kotlin)

Kotlin
fun mergeIntervals(intervals: List<IntRange>): List<IntRange> {
    if (intervals.isEmpty()) return emptyList()
    val sorted = intervals.sortedBy { it.first }
    val result = mutableListOf(sorted[0])
    for (range in sorted.drop(1)) {
        // implement merge logic
    }
    return result
}

Expect these follow-ups

  • How would you handle this if intervals could be open or half-open?
  • What is the time complexity and can it be improved?
intervalssortingalgorithmskotlinarrays

As asked

Implement a sealed class Result<out T> with Success and Failure subtypes in Kotlin. Write map, flatMap, and getOrElse extension functions on it. Then show how you would chain three operations that each may fail using this type, without nested when blocks.

Sample answer outline

The sealed class has data class Success<T>(val value: T) and data class Failure(val error: Throwable). map applies the transform only on Success and returns a new Result. flatMap chains by returning the result of the transform directly. getOrElse returns value or the default. Chaining uses flatMap { }.flatMap { } making the happy path linear and errors propagate automatically.

Reference implementation (kotlin)

Kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: Throwable) : Result<Nothing>()
}

fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = TODO()
fun <T, R> Result<T>.flatMap(transform: (T) -> Result<R>): Result<R> = TODO()
fun <T> Result<T>.getOrElse(default: T): T = TODO()

Expect these follow-ups

  • How does this compare to Kotlin's built-in Result class?
  • How would you convert a suspend function that throws into a Result using a runCatching equivalent?
sealed-classesfunctionalkotlinerror-handlingrailway

As asked

Given a directed graph as an adjacency list represented by a Map<Int, List<Int>>, write a Kotlin function that returns true if the graph contains a cycle. Use depth-first search and explain the role of each visited state.

Sample answer outline

DFS with three states: unvisited, in-progress (on the current DFS stack), and completed. A back edge (visiting an in-progress node) indicates a cycle. The candidate should use an enum or two boolean arrays for the state, iterate over all nodes as starting points to handle disconnected components, and implement with an explicit stack or recursive DFS with care for stack depth.

Reference implementation (kotlin)

Kotlin
fun hasCycle(graph: Map<Int, List<Int>>): Boolean {
    val state = mutableMapOf<Int, State>()
    enum class State { UNVISITED, IN_PROGRESS, DONE }

    fun dfs(node: Int): Boolean {
        state[node] = State.IN_PROGRESS
        for (neighbor in graph[node] ?: emptyList()) {
            // implement cycle check
        }
        state[node] = State.DONE
        return false
    }
    return graph.keys.any { state[it] == null && dfs(it) }
}

Expect these follow-ups

  • How would you detect cycles in an undirected graph?
  • How does this relate to topological sorting?
graphsdfscycle-detectionkotlinalgorithms

As asked

You receive a Flow<String> where each emission is a sentence. Write a suspend function that consumes the flow and returns a Map<String, Int> of word frequencies, normalized to lowercase and with punctuation stripped. Use Flow operators rather than collecting to a list first.

Sample answer outline

The solution uses flatMapConcat or flatMapMerge to split each sentence into a Flow<String> of words, applies map to lowercase and replace punctuation via Regex, filters blank strings, and uses fold or a toList().groupingBy().eachCount() chain. The candidate should choose flatMapConcat to preserve order and avoid overwhelming the collector.

Reference implementation (kotlin)

Kotlin
suspend fun wordFrequency(sentences: Flow<String>): Map<String, Int> {
    return sentences
        .flatMapConcat { sentence ->
            // split into words
        }
        .map { it.lowercase().replace(Regex("[^a-z]"), "") }
        .filter { it.isNotBlank() }
        // fold into a frequency map
}

Expect these follow-ups

  • How would you make this concurrent to process multiple sentences in parallel?
  • How would you stream the partial results as they arrive rather than waiting for the full flow?
flowoperatorskotlinfunctionalstring-processing

As asked

You have a counter shared between 1000 concurrent coroutines, each incrementing it 1000 times. Show how you would use Kotlin's Mutex to make this safe, and explain why AtomicInteger is often a better choice in this specific scenario.

Sample answer outline

Mutex.withLock suspends the coroutine until the lock is acquired, then releases it on completion. For a simple increment, AtomicInteger.incrementAndGet is non-blocking and avoids coroutine suspension overhead. The candidate should know that Mutex is appropriate when the protected block contains multiple operations that must be atomic, whereas AtomicInteger is sufficient for single-variable CAS operations.

Reference implementation (kotlin)

Kotlin
val mutex = Mutex()
var counter = 0

suspend fun increment() {
    mutex.withLock {
        counter++
    }
}

// alternative:
val atomicCounter = AtomicInteger(0)
fun incrementAtomic() = atomicCounter.incrementAndGet()

Expect these follow-ups

  • Can you use a synchronized block inside a coroutine?
  • What is the difference between Mutex and a ReentrantLock in terms of coroutine behavior?
mutexconcurrencykotlincoroutinesthread-safety