Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Kotlin Backend Developer Top 100 Interview Questions

100 Kotlin backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers null safety and the type system, coroutines and structured concurrency, flows, web services, data access, testing, JVM performance and system design, from the basics to production incidents.

100 questions

Questions 81-100 of 100

  1. Q81.How would you design an order service in Kotlin that never loses events?

    Save the order and its outgoing events in the same database transaction, using an outbox table, and publish events from the outbox with a separate coroutine-based publisher. The database commit then decides both the order and its events, so they can never disagree.

    flowchart LR
      A["Place order request"] --> B["Transaction: insert order and outbox rows"]
      B --> C["Commit"]
      C --> D["Outbox publisher coroutine"]
      D --> E["Message broker"]
      D --> F["Mark rows published"]
      E --> G["Consumers: payments, emails, analytics"]

    Design details:

    1. Domain model: a sealed hierarchy for order states, with transition functions that emit events as data classes.
    2. Publisher: a coroutine in the application scope that polls or listens for new outbox rows, publishes them in order and marks them sent; it stops cleanly on shutdown.
    3. At-least-once delivery: events may be published twice after a crash, so each event has a unique ID and consumers are idempotent.
    4. Ordering: publish per aggregate in order, for example by partitioning on order ID.
    5. Monitoring: alert on the age of the oldest unpublished row.
    6. Cleanup: archive published rows after a retention period.
  2. Q82.How would you design real-time price updates for clients using Kotlin flows?

    Ingest price changes from upstream into a shared, hot flow per product or channel, then let each client connection collect from it, typically over WebSockets or server-sent events. Flow operators handle conflation, filtering and backpressure for slow clients.

    flowchart LR
      A["Upstream price events"] --> B["Ingestion coroutine"]
      B --> C["State per product: latest price"]
      C --> D["Shared hot flow of changes"]
      D --> E["Client connection 1: filter subscribed products"]
      D --> F["Client connection 2: conflate for slow network"]
      E --> G["WebSocket messages"]
      F --> G

    Design details:

    1. Latest-value state: keep the current price for each product, so new subscribers receive it immediately.
    2. Conflation for slow clients: a slow client should receive the latest price, not a backlog of every change.
    3. Per-connection scopes: each connection's collector is cancelled when the client disconnects.
    4. Multiple instances: with several servers, fan out changes through a message broker so every instance receives every update.
    5. Authorization: check which products or accounts each client may subscribe to.
    6. Heartbeats detect dead connections and keep proxies from closing idle ones.

    Load test with realistic numbers of connections and update rates, since memory per connection and serialization cost dominate.

  3. Q83.How would you design scheduled jobs for a Kotlin service running on several instances?

    Every instance runs the same code, so a naive scheduler would run each job once per instance. Use a distributed lock or a lease in a shared store so exactly one instance runs each occurrence, and make jobs idempotent in case the lock is lost mid-run.

    flowchart TD
      A["Scheduler coroutine on every instance"] --> B{"Acquire lease for this job occurrence?"}
      B -->|No| C["Skip, another instance runs it"]
      B -->|Yes| D["Run the job with a timeout"]
      D --> E["Record completion and release lease"]
      D -->|"Instance crashes"| F["Lease expires, next occurrence runs elsewhere"]

    Design details:

    1. Leases with expiry in the database or another shared store, longer than the job's normal run time.
    2. Occurrence keys, such as job name plus scheduled time, so an occurrence is never run twice even if leases overlap.
    3. Idempotent jobs that can safely resume or rerun.
    4. Coroutine scheduling: a loop with delay until the next run time, computed from wall-clock time, inside the application scope.
    5. Timeouts and cancellation on shutdown.
    6. Monitoring: last successful run, duration and failures per job, with alerts when a job has not run on time.

    For many or heavy jobs, a dedicated worker deployment or a job queue keeps them away from request-serving instances.

  4. Q84.How would you design an aggregation endpoint in Kotlin that combines several backend services?

    Call the required services concurrently inside a coroutine scope, give each call its own timeout within the overall request budget, return partial results for optional parts and cache what changes slowly. Coroutines make this pattern concise and keep cancellation correct.

    flowchart TD
      A["Request with a 500 ms budget"] --> B["coroutineScope"]
      B --> C["Profile: required, 200 ms timeout"]
      B --> D["Orders: required, 300 ms timeout"]
      B --> E["Recommendations: optional, 150 ms timeout with empty fallback"]
      C --> F["Combine into response"]
      D --> F
      E --> F

    Design details:

    1. Required versus optional: a failure in a required call fails the response; an optional call falls back to an empty section.
    2. Isolation: optional calls are wrapped in their own timeout and exception handling, so they never cancel required siblings.
    3. Typed clients per service, each with connection pools, retries for transient errors and circuit breakers.
    4. Caching for slowly changing data, with request coalescing so concurrent misses trigger one load.
    5. Response shape tailored to the client, which reduces payload size and client logic.
    6. Observability: a trace span per downstream call, to see which one is slow.
  5. Q85.How would you produce a grouped sales report with Kotlin collection functions?

    Group records by the reporting key, compute aggregates for each group with sumOf, count or maxBy, then sort the results. groupingBy with eachCount or fold aggregates without building intermediate lists for each group.

    Steps:

    1. groupBy { it.region } to collect records per region.
    2. mapValues to compute revenue and the top product per group.
    3. Sort entries by revenue for presentation.
    4. Use groupingBy { ... }.eachCount() for simple counts.

    Practical notes:

    • Use Long minor units for money, so totals are exact.
    • Multiplying an Int quantity by a Long price produces a Long, avoiding overflow.
    • For large data, aggregate in the database with GROUP BY instead, and transfer only the summary.
    • Break ties deterministically when picking a top item, if equal values are possible.

    Example

    Revenue per region with its top product, and order counts per region.

    data class Sale(val region: String, val product: String, val quantity: Int, val priceMinor: Long)
    
    fun main() {
        val sales = listOf(
            Sale("north", "lamp", 2, 450),
            Sale("south", "desk", 1, 7200),
            Sale("north", "desk", 1, 7200),
            Sale("north", "lamp", 1, 450),
            Sale("south", "pen", 10, 20),
        )
    
        sales.groupBy { it.region }
            .mapValues { (_, items) ->
                val revenue = items.sumOf { it.quantity * it.priceMinor }
                val top = items.groupBy { it.product }
                    .mapValues { (_, p) -> p.sumOf { it.quantity * it.priceMinor } }
                    .maxBy { it.value }.key
                revenue to top
            }
            .entries.sortedByDescending { it.value.first }
            .forEach { (region, result) -> println("$region: revenue=${result.first} top=${result.second}") }
    
        println(sales.groupingBy { it.region }.eachCount())
    }
    // Output:
    // north: revenue=8550 top=desk
    // south: revenue=7400 top=desk
    // {north=3, south=2}
  6. Q86.How would you parse and aggregate log lines lazily in Kotlin?

    Read lines as a sequence, so each line is processed once and nothing is held in memory longer than needed, match each line against a strict pattern, skip and count malformed lines and aggregate per key.

    Steps:

    1. Use lineSequence() for text in memory, or useLines for files, which closes the file automatically.
    2. Match each line with a Regex and matchEntire, and destructure the groups.
    3. Convert fields with toInt, and treat failures as malformed lines.
    4. Aggregate with groupBy, or with groupingBy and fold for very large inputs to avoid keeping all records.
    5. Sort keys for stable output.

    Scaling further: for many files, process them in parallel with coroutines on the input and output dispatcher, then merge the per-file summaries.

    Example

    Four valid lines are aggregated per route, and one malformed line is counted and skipped.

    data class Hit(val route: String, val status: Int, val ms: Int)
    
    private val linePattern = Regex("""(\w+) (\S+) (\d{3}) (\d+)""")
    
    fun main() {
        val log = """
            GET /jobs 200 120
            GET /jobs 500 900
            POST /apply 201 300
            bad line
            GET /jobs 200 80
        """.trimIndent()
    
        var skipped = 0
        val hits = log.lineSequence().mapNotNull { line ->
            linePattern.matchEntire(line)?.destructured?.let { (method, path, status, ms) ->
                Hit("$method $path", status.toInt(), ms.toInt())
            } ?: run { skipped++; null }
        }
    
        hits.groupBy { it.route }.toSortedMap().forEach { (route, list) ->
            val avg = (list.sumOf { it.ms } + list.size / 2) / list.size
            println("$route count=${list.size} errors=${list.count { it.status >= 500 }} avg=$avg")
        }
        println("skipped=$skipped")
    }
    // Output:
    // GET /jobs count=3 errors=1 avg=367
    // POST /apply count=1 errors=0 avg=300
    // skipped=1
  7. Q87.How would you implement a token bucket rate limiter in Kotlin that is easy to test?

    Keep a token count that refills at a steady rate up to a capacity; each request takes a token or is rejected. Reading elapsed time from an injected TimeSource lets tests move time forward instantly with a test time source.

    Algorithm:

    1. On each request, add tokens for the time elapsed since the last update, capped at the capacity.
    2. Reset the time mark.
    3. If at least one token is available, take it and allow the request; otherwise reject it.

    Design points:

    • Capacity sets the allowed burst, and the refill rate sets the average.
    • Thread safety: a small synchronized section, or a Mutex in coroutine code.
    • Per-client buckets in a map keyed by client, with eviction of idle entries.
    • Several instances need shared state in a central store, or limits divided per instance.
    • Responses: status 429 with a retry hint for rejected requests.

    Example

    A burst of three is allowed, and two seconds later two tokens have refilled.

    import kotlin.time.Duration.Companion.seconds
    import kotlin.time.DurationUnit
    import kotlin.time.TestTimeSource
    import kotlin.time.TimeSource
    
    class TokenBucket(private val time: TimeSource, private val capacity: Double, private val refillPerSecond: Double) {
        private var tokens = capacity
        private var last = time.markNow()
    
        fun tryTake(): Boolean = synchronized(this) {
            tokens = minOf(capacity, tokens + last.elapsedNow().toDouble(DurationUnit.SECONDS) * refillPerSecond)
            last = time.markNow()
            if (tokens >= 1) {
                tokens -= 1
                true
            } else {
                false
            }
        }
    }
    
    fun main() {
        val clock = TestTimeSource()
        val bucket = TokenBucket(clock, capacity = 3.0, refillPerSecond = 1.0)
    
        println(List(5) { bucket.tryTake() }) // [true, true, true, false, false]
        clock += 2.seconds
        println(List(3) { bucket.tryTake() }) // [true, true, false]
    }
  8. Q88.How would you order jobs by their dependencies in Kotlin?

    Produce a topological order of the dependency graph: track how many unfinished dependencies each job has, start with jobs that have none and, as each job is taken, reduce the count for the jobs that depend on it. If some jobs never become ready, the graph has a cycle.

    Steps:

    1. Build a map from each job to its dependents, and a count of remaining dependencies per job.
    2. Put all jobs with zero dependencies into a sorted set, so the order is deterministic.
    3. Repeatedly take the first ready job, append it to the result and update its dependents.
    4. Fail with a clear error if the result is shorter than the number of jobs.

    Uses: build steps, database migrations, deployment pipelines and data processing tasks where some steps need others first.

    Parallel execution: all jobs that are ready at the same moment can run concurrently, for example as coroutines in one scope, while still respecting dependencies.

    Example

    Independent jobs come first alphabetically, and a cycle is reported.

    fun order(deps: Map<String, List<String>>): List<String> {
        val remaining = deps.mapValues { it.value.size }.toMutableMap()
        val dependents = deps.keys.associateWith { mutableListOf<String>() }
        deps.forEach { (job, needs) -> needs.forEach { dependents.getValue(it) += job } }
    
        val ready = sortedSetOf<String>().apply { addAll(remaining.filterValues { it == 0 }.keys) }
        val result = mutableListOf<String>()
        while (ready.isNotEmpty()) {
            val job = ready.first()
            ready.remove(job)
            result += job
            dependents.getValue(job).forEach { next ->
                remaining[next] = remaining.getValue(next) - 1
                if (remaining[next] == 0) ready += next
            }
        }
        check(result.size == deps.size) { "cycle detected" }
        return result
    }
    
    fun main() {
        val jobs = mapOf(
            "deploy" to listOf("test", "build"),
            "test" to listOf("build"),
            "build" to listOf("restore"),
            "restore" to emptyList(),
            "docs" to emptyList(),
        )
        println(order(jobs).joinToString(" -> ")) // docs -> restore -> build -> test -> deploy
    
        try {
            order(mapOf("a" to listOf("b"), "b" to listOf("a")))
        } catch (e: IllegalStateException) {
            println(e.message) // cycle detected
        }
    }
  9. Q89.How would you compute moving averages and percentiles over latency samples in Kotlin?

    Use windowed to compute statistics over sliding windows of samples, and a sorted copy of the samples with a nearest-rank rule for percentiles. Moving averages smooth noise, while percentiles show the tail latency that users at the slow end experience.

    Techniques:

    1. windowed(size) { ... } produces one value per window, such as the average of the last three samples.
    2. Percentiles: sort the samples, then take the value at rank ceil(p / 100 × n).
    3. Flagging anomalies: find windows whose average exceeds a threshold, using withIndex to report their positions.

    Practical notes:

    • Averages hide spikes: one slow request barely moves the average of many fast ones, but it decides the 95th percentile of a small sample.
    • For production metrics, use histograms that can be merged across instances, rather than storing every sample.
    • Use enough samples: percentiles of a handful of values are unstable.

    Example

    One slow request lifts three moving windows, and the percentiles show the difference between typical and tail latency.

    import kotlin.math.ceil
    
    fun percentile(values: List<Int>, p: Double): Int {
        val sorted = values.sorted()
        val rank = ceil(p / 100 * sorted.size).toInt().coerceIn(1, sorted.size)
        return sorted[rank - 1]
    }
    
    fun main() {
        val latencies = listOf(120, 130, 125, 900, 140, 135, 128, 132)
    
        println(latencies.windowed(3) { it.average().toInt() })                      // [125, 385, 388, 391, 134, 131]
        println(latencies.windowed(3).withIndex().filter { it.value.average() > 300 }.map { it.index }) // [1, 2, 3]
        println("p50=${percentile(latencies, 50.0)} p95=${percentile(latencies, 95.0)}") // p50=130 p95=900
    }
  10. Q90.How would you build a small type-safe event bus in Kotlin?

    Model events as a sealed hierarchy, keep handlers in a map keyed by event class and offer a subscribe function with a reified type parameter, so callers register handlers for a specific event type without casts at the call site.

    Design points:

    1. Sealed events make the set of event types explicit, and data classes carry their fields.
    2. Reified subscribe<E> captures the event class at the call site and wraps the handler with a safe cast.
    3. Publishing looks up handlers by the event's class and calls each one.
    4. Error isolation: in production, catch and log failures per handler, so one broken handler does not stop the others.
    5. Asynchronous handlers: for slow work, publish to a channel or flow, and let handlers collect in their own coroutines.

    Limits: an in-process bus loses events on crashes and does not reach other instances; use a durable message broker or an outbox for events that must survive and travel between services.

    Example

    Two handlers receive the placed event, one receives the cancelled event and none needs a cast.

    import kotlin.reflect.KClass
    
    sealed interface DomainEvent {
        data class OrderPlaced(val id: String) : DomainEvent
        data class OrderCancelled(val id: String, val reason: String) : DomainEvent
    }
    
    class EventBus {
        private val handlers = mutableMapOf<KClass<out DomainEvent>, MutableList<(DomainEvent) -> Unit>>()
    
        inline fun <reified E : DomainEvent> subscribe(noinline handler: (E) -> Unit) =
            register(E::class) { handler(it as E) }
    
        fun register(type: KClass<out DomainEvent>, handler: (DomainEvent) -> Unit) {
            handlers.getOrPut(type) { mutableListOf() } += handler
        }
    
        fun publish(event: DomainEvent) {
            handlers[event::class]?.forEach { it(event) }
        }
    }
    
    fun main() {
        val bus = EventBus()
        bus.subscribe<DomainEvent.OrderPlaced> { println("email confirmation for ${it.id}") }
        bus.subscribe<DomainEvent.OrderPlaced> { println("reserve stock for ${it.id}") }
        bus.subscribe<DomainEvent.OrderCancelled> { println("refund ${it.id}: ${it.reason}") }
    
        bus.publish(DomainEvent.OrderPlaced("ord-1"))
        bus.publish(DomainEvent.OrderCancelled("ord-2", "customer request"))
    }
    // Output:
    // email confirmation for ord-1
    // reserve stock for ord-1
    // refund ord-2: customer request
  11. Q91.How would you keep only the latest version of each record from out-of-order updates in Kotlin?

    Group updates by key and keep the one with the highest version, rather than the one that arrived last. Messages from queues and webhooks can arrive out of order, so "last write wins" by arrival time can overwrite new data with old data.

    Approach:

    1. Give every update a monotonically increasing version or a reliable timestamp from the source.
    2. Group by key with groupBy, then pick maxBy { it.version } for each key.
    3. When applying updates one at a time to a store, compare the incoming version with the stored one, and ignore older updates.

    In a database: a conditional update, UPDATE ... SET price = :price, version = :version WHERE sku = :sku AND version < :version, applies the rule atomically.

    Batching for downstream writes: after deduplication, chunked(n) splits the records into batches for efficient bulk writes.

    Example

    The newest version wins even though an older update for the same product arrives last, and the results are split into batches.

    data class PriceUpdate(val sku: String, val version: Int, val priceMinor: Long)
    
    fun main() {
        val updates = listOf(
            PriceUpdate("LAMP", 1, 45_000),
            PriceUpdate("DESK", 1, 720_000),
            PriceUpdate("LAMP", 3, 43_000),
            PriceUpdate("LAMP", 2, 44_000),
            PriceUpdate("PEN", 1, 2_000),
        )
    
        val latest = updates.groupBy { it.sku }.mapValues { (_, list) -> list.maxBy { it.version } }.values.sortedBy { it.sku }
        println(latest.map { "${it.sku}=${it.priceMinor}" }) // [DESK=720000, LAMP=43000, PEN=2000]
        println(latest.map { it.sku }.chunked(2))            // [[DESK, LAMP], [PEN]]
    }
  12. Q92.How do you migrate an existing JVM codebase to Kotlin gradually?

    Kotlin and existing JVM code can live in the same project and call each other directly, so migration can happen file by file, while the system keeps shipping. Start where the risk is low and the benefit is clear, and set rules for interop boundaries.

    A practical sequence:

    1. Set up the build to compile both languages, and add Kotlin formatting and static analysis.
    2. Write new code in Kotlin, such as new features and tests, so the team learns the language.
    3. Convert tests first in some areas, which carries little production risk.
    4. Convert leaf classes, such as data holders and utilities, then services.
    5. Add nullability annotations to the remaining existing code, so Kotlin sees accurate types instead of platform types.
    6. Review automatic conversions: converted code compiles but is rarely idiomatic; refactor to data classes, null safety and sealed types.

    Interop rules:

    • Expose clean APIs for callers in the other language, using annotations for static members, overloads and declared exceptions.
    • Avoid leaking Kotlin-specific types, such as default arguments without overloads, to code that cannot use them easily.

    Measure build times, since mixed builds can be slower, and track defect rates to show the benefit.

  13. Q93.How do you upgrade Kotlin versions safely in a backend project?

    Upgrade regularly, read the release notes for language and compiler changes, align related plugin and library versions and run the full test suite and static analysis before rolling out.

    1. Check compatibility: Kotlin releases follow a predictable schedule, with language releases introducing features and deprecations, and tooling releases fixing issues; deprecations usually progress from warning to error over several releases.
    2. Upgrade the compiler, plugins and standard library together, including serialization and other compiler plugins, which must match the compiler version.
    3. Check library compatibility: libraries compiled with a newer Kotlin version may require a newer compiler to read their metadata.
    4. Fix deprecation warnings promptly, before they become errors in later versions.
    5. Run tests, static analysis and benchmarks, since compiler changes can affect performance and behavior in edge cases.
    6. Watch build performance, which can change with new compiler versions, especially with the new compiler front end.
    7. Roll out gradually for services, as with any change that affects all code.

    Language version settings let you upgrade the compiler while keeping older language behavior temporarily, which helps large codebases migrate in steps.

  14. Q94.What do you look for when reviewing Kotlin backend code?

    Focus review time on concurrency, null safety at boundaries, error handling and domain modeling, and let formatters and static analysis catch style problems.

    Checklist:

    1. Coroutines: no GlobalScope, no runBlocking in request paths, blocking calls wrapped in the right dispatcher and cancellation never swallowed.
    2. Null safety: no unexplained !!, explicit types at interop boundaries and validation that turns input into non-null domain types.
    3. Errors: expected failures modeled as sealed results, exceptions handled centrally and no broad catch that hides problems.
    4. Domain modeling: sealed states instead of flag combinations, value classes for identifiers and val by default.
    5. Collections: read-only types in public APIs, no huge intermediate lists in hot paths and correct use of sequences.
    6. Concurrency: shared mutable state protected, with Mutex rather than blocking locks in suspending code.
    7. Data access: parameterized queries, no N+1 patterns and short transactions.
    8. Readability: scope functions used sparingly, no deep nesting and clear names.
    9. Tests: deterministic time, coroutine tests with virtual time and failure paths covered.

    Interview tip: pointing out a catch (e: Exception) around a suspending call as a cancellation bug is a quick way to show deep coroutine knowledge in a code review exercise.

  15. Q95.How do you choose between coroutines, threads and reactive streams in a Kotlin service?

    Choose the concurrency model by the libraries you depend on, the team's experience and the kind of work. In Kotlin, coroutines are usually the default, but the other models still have their place.

    CoroutinesPlain threadsReactive streams
    Sequential-looking asynchronous code with structured concurrencySimple blocking code on thread pools or virtual threadsAsynchronous pipelines built from operators
    Best for most new Kotlin services and suspending librariesBest for blocking libraries and simple batch toolsBest for codebases already built on reactive libraries
    Cancellation, timeouts and scoping built inCancellation and scoping are manualRich operators, but harder to read and debug

    Practical guidance:

    1. New Kotlin services: coroutines with suspending clients for databases and HTTP.
    2. Existing reactive code: adapters convert between reactive types and coroutines or flows, so migration can be gradual.
    3. Blocking libraries without alternatives: wrap them on the input and output dispatcher, or use virtual threads.
    4. Consistency matters: mixing all three models in one service makes behavior hard to reason about.

    Whatever the model, downstream limits still apply; cheap concurrency does not make databases or partner APIs faster.

  16. Q96.How do you handle backpressure in Kotlin flows and channels?

    Backpressure is what happens when a producer is faster than its consumer. In a plain flow, the producer simply suspends until the collector is ready, which is safe but can slow the producer. Buffering and conflation operators change that behavior explicitly.

    buffer(n)conflate()collectLatest
    Lets the producer run ahead by up to n itemsKeeps only the most recent value when the collector is busyCancels the previous processing when a new value arrives
    No data loss, within the buffer sizeIntermediate values are droppedOnly the latest value's processing completes
    Best for smoothing bursts of work that all matterBest for state updates, such as prices or progressBest for search-as-you-type and latest-request-wins

    For channels:

    • A bounded capacity suspends senders when full, which is the safest default.
    • Overflow strategies can drop the oldest or the newest item instead, for data where losing some values is acceptable.
    • Avoid unlimited buffers in services, since they only move the problem to memory.

    Choose deliberately: decide for each stream whether losing values is acceptable, and monitor buffer sizes and drops.

  17. Q97.How do you keep a Kotlin library binary-compatible for its users?

    Binary compatibility means code compiled against an older version of your library still runs with the new version without recompiling. Kotlin features, such as default arguments, inline functions and data classes, can break it in ways source compatibility checks miss.

    Common breaking changes:

    1. Adding a parameter with a default value changes the compiled method signature for some callers.
    2. Changing an inline function's body does not affect callers already compiled with the old body, which can cause inconsistent behavior.
    3. Adding a property to a data class constructor changes copy and component functions.
    4. Making a class final, or changing visibility, breaks subclasses or callers.

    Practices:

    • Explicit API mode forces visibility modifiers and return types on public declarations, so nothing is exposed by accident.
    • A binary compatibility validator in the build compares the public API dump with the previous release and fails on unintended changes.
    • Deprecate before removing, with replacement hints, and keep deprecated APIs for at least one release.
    • Avoid data classes in public APIs that must evolve; use regular classes with explicit factories.
    • Semantic versioning: breaking changes only in major versions.
  18. Q98.How do you design a domain-specific language in Kotlin, and when is it worth it?

    Kotlin DSLs are built from lambdas with receivers, extension functions and builder classes, so configuration reads like structured text while staying type-checked. They suit repetitive, declarative setup, such as routes, test data, HTML or build configuration.

    Building blocks:

    1. A builder class that collects configuration, such as routes or fields.
    2. A function taking a lambda with receiver, fun router(block: RouterBuilder.() -> Unit), which runs the block with the builder as this.
    3. Member functions on the builder for each construct, such as get and post.
    4. A DSL marker annotation that prevents accidentally calling outer builders from nested blocks.

    When it is worth it:

    • The same structure is written many times, and the DSL removes boilerplate.
    • Type safety catches mistakes that string or file-based configuration would only reveal at run time.

    When it is not: for one-off setup, plain functions and data classes are clearer. DSLs can hide behavior, so keep them small, documented and tested.

    Example

    A tiny routing DSL declares routes with a lambda with receiver, then a handler runs with path parameters.

    class Route(val method: String, val path: String, val handler: (Map<String, String>) -> String)
    
    class RouterBuilder {
        val routes = mutableListOf<Route>()
        fun get(path: String, handler: (Map<String, String>) -> String) {
            routes += Route("GET", path, handler)
        }
        fun post(path: String, handler: (Map<String, String>) -> String) {
            routes += Route("POST", path, handler)
        }
    }
    
    fun router(block: RouterBuilder.() -> Unit): List<Route> = RouterBuilder().apply(block).routes
    
    fun main() {
        val routes = router {
            get("/orders/{id}") { params -> "order ${params["id"]}" }
            post("/orders") { "created" }
        }
        routes.forEach { println("${it.method} ${it.path}") }
        println(routes.first().handler(mapOf("id" to "42")))
    }
    // Output:
    // GET /orders/{id}
    // POST /orders
    // order 42
  19. Q99.How should you explain a Kotlin backend project you personally worked on?

    Tell one project as a story: the problem, your role, the key decisions, the hardest technical challenge and measurable results. Interviewers want to hear how you used Kotlin's strengths, such as coroutines and the type system, to build something reliable.

    1. Context: what the service did, its users, traffic and data volume.
    2. Your role: what you personally designed or built.
    3. Decisions: how you modeled the domain, structured concurrency, accessed data and handled errors, and why.
    4. A hard problem: blocked dispatchers, a coroutine leak, a cancellation bug, a migration from older JVM code or a performance issue, and the evidence that led to the cause.
    5. Quality: tests with virtual time, integration tests, static analysis and deployment practices.
    6. Results: numbers such as "p99 latency fell from 900 ms to 180 ms" or "null pointer errors in production dropped to zero after the migration".
    7. Reflection: what you would do differently now.

    What the interviewer is listening for:

    • Idiomatic use of Kotlin, not old patterns rewritten in a new syntax.
    • Understanding of coroutines beyond the basics: scopes, dispatchers and cancellation.
    • Clear trade-offs and measurable impact.

    Interview tip: prepare a short sketch of your service's coroutine scopes and dispatchers, since follow-up questions often go there.

  20. Q100.How do you discuss a production incident you handled in a Kotlin service?

    Walk through the incident from detection to prevention, showing how you restored service first and then found the cause with evidence such as thread dumps, coroutine dumps, heap dumps, traces and metrics.

    1. Detection: the alert or report, and how quickly it arrived.
    2. Impact: what users experienced, how many and for how long.
    3. Mitigation: rollback, scaling, a feature switch or rate limiting, and why you chose it.
    4. Diagnosis: for example, a thread dump showing default dispatcher threads blocked in a synchronous client, or a coroutine dump showing thousands of coroutines waiting on the same channel.
    5. Root cause and contributing factors: such as a new library with blocking calls, a swallowed cancellation or a missing timeout.
    6. Fix: the change, plus a test that reproduces the problem with virtual time or a fake.
    7. Prevention: blocking-call detection in tests, alerts on dispatcher queue time, lint rules and review checklist items.

    What the interviewer is listening for:

    • Calm, user-first priorities.
    • Evidence-based diagnosis with the right JVM and coroutine tools.
    • Blameless, system-focused language.
    • Lasting improvements that were verified afterwards.