Live Pair Coding — 40 min

Komoju is a payments shop. Likely problems:

  • Polling / retry / backoff with Flow and runTest (very likely — they saw your take-home).
  • State machine (PENDING → PAID/EXPIRED/FAILED with rules).
  • Concurrency tool (debounce, throttle, single-flight, dedup-by-key).
  • Small algorithm with a twist (e.g., "build an idempotency-key cache with TTL").

First 60 seconds

"Before I write code: let me clarify the inputs and outputs, sketch the idea in pseudocode, then code, then test. Sound good?"

Always:

  1. Repeat the problem in your own words.
  2. Ask about edge cases: empty input, concurrency, cancellation, error.
  3. Ask about limits: time/space, can I add a library, target Kotlin version.
  4. Talk while you code. Silence is the killer.

Cheat-sheet — Flow / coroutines patterns

Polling that respects cancellation

fun poll(intervalMs: Long): Flow<Status> = flow {
    while (currentCoroutineContext().isActive) {
        val s = api.status()
        emit(s)
        if (s.isTerminal) return@flow
        delay(intervalMs)
    }
}

Retry with exponential backoff

fun <T> Flow<T>.retryWithBackoff(
    maxAttempts: Int = 5,
    initial: Long = 1_000L,
    factor: Double = 2.0,
    max: Long = 30_000L,
): Flow<T> = retryWhen { cause, attempt ->
    if (cause is CancellationException || attempt >= maxAttempts) return@retryWhen false
    val delayMs = (initial * factor.pow(attempt.toInt())).toLong().coerceAtMost(max)
    delay(delayMs)
    true
}

Single-flight / dedup-by-key

One in-flight call per key. New callers join the same call.

class SingleFlight<K, V> {
    private val mutex = Mutex()
    private val inflight = mutableMapOf<K, Deferred<V>>()

    suspend fun run(key: K, block: suspend () -> V): V {
        val deferred = mutex.withLock {
            inflight[key] ?: coroutineScope { async { block() } }.also { inflight[key] = it }
        }
        return try { deferred.await() } finally { mutex.withLock { inflight.remove(key) } }
    }
}

runTest with virtual time

@Test
fun `polling stops on PAID`() = runTest {
    val api = FakeApi(scripted = listOf(PENDING, PENDING, PAID))
    val emissions = repo.poll().toList()
    assertEquals(listOf(PENDING, PENDING, PAID), emissions)
}

StateFlow updates without races

_state.update { current -> current.copy(loading = false, data = data) }   // atomic CAS

Per-key mutex (when one global mutex is too coarse)

class KeyedMutex<K> {
    private val mutexes = ConcurrentHashMap<K, Mutex>()
    suspend fun <V> withLock(key: K, block: suspend () -> V): V {
        val m = mutexes.computeIfAbsent(key) { Mutex() }
        return m.withLock { block() }
    }
}

Debounce with Flow

input.debounce(300.milliseconds).distinctUntilChanged().collect { search(it) }

Throttle (latest wins) with conflate

upstream.conflate().collect { /* skips middle values when slow */ }

Channel-based producer-consumer

val channel = Channel<Job>(Channel.BUFFERED)
launch { for (job in channel) process(job) }   // single consumer
launch { repeat(N) { channel.send(makeJob()) } }

Common traps to avoid

  • Don't catch CancellationException — always rethrow. They WILL test this.
    } catch (e: CancellationException) { throw e
    } catch (e: Exception) { /* handle */ }
    
  • Don't runBlocking inside a suspend function — show you know withContext.
  • Don't use GlobalScope — instant red flag.
  • Avoid !! — use ?.let, requireNotNull("msg"), or a sealed-class state.
  • Avoid delay for sync — use Channel, Flow, or Mutex.
  • Test with runTest, not runBlocking — virtual time matters.
  • Don't hold a mutex across await() — risk of deadlock if the awaited work needs the same mutex.
  • Pick the right dispatcher — IO for blocking, Default for CPU. Say you thought about it.

Likely problem variants — quick mental rehearsal

1. "Write a function that polls until terminal, with backoff, given a suspend () -> Status."

This is your repo code, simpler. Be ready to write it from scratch in 10 minutes. Cover: cancellation, terminal stop, backoff cap, mention jitter.

2. "Given two flows, emit only when both have a value, and re-emit on either change."

Answer: combine(flowA, flowB) { a, b -> Pair(a, b) }.

3. "Build a rate limiter: at most N calls per second, queue the rest."

Channel + ticker, or Semaphore(N), or a sliding-window timestamp queue.

class RateLimiter(private val permits: Int, private val perMillis: Long) {
    private val timestamps = ArrayDeque<Long>()
    private val mutex = Mutex()
    suspend fun acquire() = mutex.withLock {
        val now = System.currentTimeMillis()
        while (timestamps.isNotEmpty() && now - timestamps.first() > perMillis) timestamps.removeFirst()
        if (timestamps.size >= permits) {
            delay(perMillis - (now - timestamps.first()))
        }
        timestamps.addLast(System.currentTimeMillis())
    }
}

4. "Detect and dedup duplicate events that arrive within X ms."

debounce(X.milliseconds) for last-wins, or a custom seen-set with TTL.

5. "Cache results with TTL, single-flight on miss."

Mix SingleFlight + a TTL map:

class TtlCache<K, V>(private val ttl: Duration, private val source: suspend (K) -> V) {
    private data class Entry<V>(val value: V, val expiresAt: Long)
    private val map = ConcurrentHashMap<K, Entry<V>>()
    private val flight = SingleFlight<K, V>()

    suspend fun get(key: K): V {
        val cached = map[key]
        if (cached != null && System.currentTimeMillis() < cached.expiresAt) return cached.value
        return flight.run(key) {
            source(key).also { map[key] = Entry(it, System.currentTimeMillis() + ttl.inWholeMilliseconds) }
        }
    }
}

6. "Build an idempotency-key wrapper for an API call."

Server side: dedup by key, return cached response. Client side: keep the same key across retries.

suspend fun <T> withIdempotencyKey(key: String, block: suspend (key: String) -> T): T {
    return block(key)   // server stores the response under `key` for some time window
}

7. "Two coroutines append to a list — make it safe."

  • Mutex().withLock { list.add(x) }
  • Or Channel to serialize
  • Or MutableStateFlow.update
  • NOT just synchronized — show you know coroutine-friendly tools.

8. "Race two API calls and take whichever finishes first; cancel the loser."

suspend fun <T> race(a: suspend () -> T, b: suspend () -> T): T = coroutineScope {
    select {
        async { a() }.onAwait { it }
        async { b() }.onAwait { it }
    }.also { coroutineContext.cancelChildren() }
}

If you're stuck

"Let me think out loud. I'm choosing between A and B. A is simpler but doesn't handle X. B handles X but is more code. Which one do you care about more?"

Asking for a tradeoff is not weakness. It's a senior signal.


What they're scoring

  • Communication: do you narrate? Do you ask before you assume?
  • Correctness: does it work for the obvious test cases?
  • Edge cases: empty, single, concurrent, cancelled, error.
  • Concurrency: cancellation, dispatcher, structured concurrency.
  • Testing: do you write or at least sketch a test?
  • Refactor after it works: spot duplication, naming, helpers — and don't over-refactor.

results matching ""

    No results matching ""