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 61-80 of 100

  1. Q61.How do you cache data in a Kotlin service?

    Cache data that is read often, changes rarely and is expensive to compute or fetch. Use a bounded in-process cache for per-instance speed and a shared cache when every instance must see the same values, and always define expiry and size limits.

    Design points:

    1. Bounded size with an eviction policy, such as least recently used, so memory cannot grow without limit.
    2. Expiry that matches how stale the data may be.
    3. Concurrent loading: when many coroutines miss the same key, let one load it while the others wait for the same result, for example by caching a deferred value.
    4. Keys that include every input affecting the value, such as tenant and language.
    5. Invalidation when the source changes, or short lifetimes when change events are not available.
    6. Metrics: hit rate, size and load time.

    A simple in-process LRU can be built on a linked hash map in access order, which moves an entry to the end whenever it is read and lets you remove the eldest entry when the size limit is exceeded. It is not thread-safe, so guard it with a lock or use a concurrent cache library in production.

    Example

    A linked hash map in access order evicts the least recently used entry.

    class LruCache<K, V>(private val capacity: Int) : LinkedHashMap<K, V>(16, 0.75f, true) {
        override fun removeEldestEntry(eldest: MutableMap.MutableEntry<K, V>?): Boolean = size > capacity
    }
    
    fun main() {
        val cache = LruCache<String, Int>(2)
        cache["a"] = 1
        cache["b"] = 2
        cache["a"]
        cache["c"] = 3
    
        println(cache.keys) // [a, c]
        println(cache["b"]) // null
    }
  2. Q62.How do you secure a Kotlin web API?

    Layer defenses: authenticate every request, authorize every action on every resource, validate input strictly, protect secrets and keep dependencies patched. Frameworks provide the building blocks, but secure defaults must be configured deliberately.

    Authentication and authorization:

    1. Validate tokens fully: signature, issuer, audience and expiry.
    2. Require authentication by default, and mark public routes explicitly.
    3. Check ownership in handlers that load resources by ID, so users cannot read each other's data by changing an ID.
    4. Use role or permission checks expressed as small, testable functions.

    Input and output:

    • Validate request models into typed commands, reject unknown fields where appropriate and limit body sizes.
    • Use parameterized queries and allowlists for dynamic sorting.
    • Return consistent errors without stack traces or internal details.

    Transport and platform:

    • HTTPS everywhere, strict CORS rules and security headers.
    • Rate limits on login and expensive endpoints.

    Secrets and dependencies:

    • Secrets from the environment or a secret store, never in code or logs.
    • Dependency verification and vulnerability scanning in the build, and regular updates of the JVM and libraries.
  3. Q63.How do you load and validate configuration in a Kotlin service?

    Read configuration from the environment and configuration files at startup, convert values to typed fields, validate them all and fail fast with every problem listed. The rest of the code receives an immutable configuration data class, never raw strings.

    Practices:

    1. One typed data class with Duration, Int, Boolean and value class fields, not strings.
    2. Defaults for optional settings and clear errors for required ones.
    3. Collect all errors before failing, so operators fix everything in one deployment.
    4. Testable loading: pass the environment as a map into the loader instead of reading global state.
    5. Secrets from the environment or a secret store, never committed, and never printed when logging the configuration.
    6. Separate configuration per environment through values, not through different code paths.

    Using Duration for timeouts avoids the classic mistake of mixing milliseconds and seconds.

    Example

    Valid settings produce a typed configuration; invalid ones fail with every problem listed at once.

    import kotlin.time.Duration
    import kotlin.time.Duration.Companion.seconds
    
    data class AppConfig(val port: Int, val dbUrl: String, val requestTimeout: Duration, val debug: Boolean)
    
    fun loadConfig(env: Map<String, String>): AppConfig {
        val errors = mutableListOf<String>()
        fun <T : Any> read(name: String, default: T?, parse: (String) -> T?): T? {
            val raw = env[name] ?: return default.also { if (it == null) errors += "$name is required" }
            return parse(raw).also { if (it == null) errors += "$name is invalid: '$raw'" }
        }
        val port = read("PORT", 8080) { it.toIntOrNull()?.takeIf { p -> p in 1..65535 } }
        val dbUrl = read<String>("DB_URL", null) { it.takeIf(String::isNotBlank) }
        val timeout = read("REQUEST_TIMEOUT", 5.seconds) { Duration.parseOrNull(it) }
        val debug = read("DEBUG", false) { it.toBooleanStrictOrNull() }
        require(errors.isEmpty()) { errors.joinToString("; ") }
        return AppConfig(port!!, dbUrl!!, timeout!!, debug!!)
    }
    
    fun main() {
        println(loadConfig(mapOf("PORT" to "9000", "DB_URL" to "db.internal/shop", "REQUEST_TIMEOUT" to "2s")))
        try {
            loadConfig(mapOf("PORT" to "99999", "REQUEST_TIMEOUT" to "soon"))
        } catch (e: IllegalArgumentException) {
            println(e.message)
        }
    }
    // Output:
    // AppConfig(port=9000, dbUrl=db.internal/shop, requestTimeout=2s, debug=false)
    // PORT is invalid: '99999'; DB_URL is required; REQUEST_TIMEOUT is invalid: 'soon'

    The !! operators at the end are safe because any missing value would already have failed the require check.

  4. Q64.How should a Kotlin service log and trace requests?

    Write structured logs, such as JSON with named fields, through a logging facade, and add distributed tracing so a request can be followed across services. With coroutines, request context must travel in the coroutine context rather than in thread-local storage, because a coroutine can resume on a different thread.

    Logging practices:

    1. Structured fields, such as orderId and durationMs, rather than values embedded in message text.
    2. A request ID and trace ID on every log line, added by middleware.
    3. Context propagation with coroutines: use a context element that copies diagnostic context into each thread a coroutine runs on, or the framework's built-in support.
    4. Lazy messages: pass lambdas for expensive debug messages, so the string is built only when the level is enabled.
    5. No secrets or sensitive personal data in logs.
    6. Log exceptions once, at the boundary, with the stack trace.

    Tracing practices:

    • Create spans for incoming requests, database calls and outgoing HTTP calls.
    • Propagate trace headers to downstream services.
    • Make sure spans follow coroutines across thread switches, using the tracing library's coroutine integration.
  5. Q65.How do you write unit tests for Kotlin backend code?

    Test behavior through public functions with fast, isolated tests that use fakes for external systems. Kotlin's test libraries and the multiplatform test module provide assertions, while expressive test names and data classes make tests easy to read.

    Structure:

    1. Arrange: build the object with fakes and inputs.
    2. Act: call one function.
    3. Assert: compare results, often whole data classes, which gives clear failure messages.

    Kotlin-specific tips:

    • Readable names: backtick-quoted names, such as `rejects refunds after 30 days`, read like specifications.
    • Data class comparisons check every field at once.
    • Sealed results are easy to assert with when or type checks.
    • Coroutine code runs in runTest with virtual time.
    • Parameterized tests cover many inputs for the same rule.

    Good habits:

    • Inject clocks, ID generators and random sources, so tests are deterministic.
    • Test failure paths, including cancellation and timeouts.
    • Keep unit tests free of real databases and networks; cover those with integration tests against real engines in containers.
    • Avoid mocking data classes or value types; build real instances.
  6. Q66.How do you replace dependencies with fakes in Kotlin tests?

    Define small interfaces for external dependencies, then write simple fake classes in tests that record calls and return chosen results. Kotlin makes fakes short: an interface with one method can be implemented by a lambda, and data classes make recorded calls easy to compare.

    Kinds of test doubles:

    • Fakes: working in-memory implementations, such as a repository backed by a map.
    • Stubs: fixed answers or failures, to drive specific paths.
    • Spies: record calls for later checks.

    Guidelines:

    1. Fake what you do not own: payment gateways, email services, clocks and remote APIs.
    2. Check outcomes rather than every interaction, so refactoring does not break tests.
    3. Share fakes across tests to keep them consistent.
    4. Mocking libraries are useful for large interfaces and for final classes, but hand-written fakes are often clearer.
    5. Run contract tests against both the fake and the real implementation, so they behave the same.

    Example

    A configurable fake gateway drives both the success and the decline path, and records what was charged.

    interface PaymentGateway {
        fun charge(orderId: String, amountMinor: Long): Boolean
    }
    
    class FakeGateway(private val declineAbove: Long) : PaymentGateway {
        val charges = mutableListOf<String>()
    
        override fun charge(orderId: String, amountMinor: Long): Boolean {
            if (amountMinor > declineAbove) return false
            charges += "$orderId:$amountMinor"
            return true
        }
    }
    
    class Checkout(private val payments: PaymentGateway) {
        fun pay(orderId: String, amountMinor: Long): String =
            if (payments.charge(orderId, amountMinor)) "paid" else "declined"
    }
    
    fun main() {
        val gateway = FakeGateway(declineAbove = 100_000)
        val checkout = Checkout(gateway)
    
        println(checkout.pay("ord-1", 45_000))  // paid
        println(checkout.pay("ord-2", 250_000)) // declined
        println(gateway.charges)                // [ord-1:45000]
    }
  7. Q67.How do you test HTTP endpoints in a Kotlin service?

    Most Kotlin web frameworks provide an in-memory test host: the application starts with test configuration, and tests send requests through a client without opening real network ports. The whole pipeline runs, including routing, serialization, validation, authentication and error handling.

    Setting it up:

    1. Start the application module with test settings and fake external dependencies.
    2. Send requests with the test client, including headers and JSON bodies.
    3. Assert on status codes, headers and decoded response bodies, comparing to data classes.

    What to cover:

    • Success paths for each endpoint.
    • Validation errors with field messages.
    • Authentication failures, missing permissions and access to other users' resources.
    • Error mapping, such as 404 and 409 for domain failures, and a generic 500 without internal details.
    • Serialization details, such as naming, null handling and unknown fields.

    Database-backed tests: run them against the real database engine in a container, and reset data between tests.

    Keep them fast: share one application instance per test class where possible, and run independent tests in parallel.

  8. Q68.How do you handle dates, times and time zones in Kotlin services?

    Store and exchange instants in UTC, use the platform's modern immutable date and time types and convert to a user's zone only for display. Keep calendar dates, such as birthdays, separate from instants, such as when an order was placed.

    Types to use for each purpose:

    • Instants for timestamps in databases, logs and APIs.
    • Local dates for calendar dates without a time.
    • Zoned date-times when local rules matter, such as scheduling a meeting in a specific city.
    • Duration from the Kotlin standard library for amounts of time in code, such as timeouts.

    Good practices:

    1. Inject a clock, so code reading "now" is testable.
    2. Use standard date-time strings in APIs, such as 2026-03-01T09:30:00Z.
    3. Convert time zones by zone ID, which handles daylight saving rules, not by fixed offsets.
    4. Be careful with "add one day" across daylight saving changes: adding 24 hours and adding a calendar day can differ.
    5. Multiplatform code can use a date-time library that works on every Kotlin target.

    Recurring local events, such as "every day at 9 in the user's zone", should be stored as a local time plus a zone, and converted to instants when each occurrence is scheduled.

  9. Q69.How do you represent money and precise amounts in Kotlin?

    Never use Double for money: binary floating point cannot represent most decimal fractions exactly, so errors accumulate. Use integer amounts in the smallest currency unit, wrapped in a value class, or the platform's arbitrary-precision decimal type with explicit rounding.

    A money type should:

    1. Store minor units as a Long, such as paise or cents, with the currency.
    2. Refuse to mix currencies in arithmetic.
    3. Round explicitly, in one place, for operations such as tax and discounts.
    4. Allocate remainders when splitting, so the parts always add up to the total.
    5. Format only for display, with the right locale.

    Storage and APIs: store integers or exact decimal columns in the database; in JSON, send minor units as integers or decimal amounts as strings, so clients do not lose precision.

    Value classes give a distinct type for money with almost no runtime cost, and operator functions make arithmetic read naturally.

    Example

    Integer minor units give exact totals, explicit rounding for tax and splits that always add up.

    @JvmInline
    value class Paise(val value: Long) {
        operator fun plus(other: Paise) = Paise(value + other.value)
        operator fun times(quantity: Int) = Paise(value * quantity)
        fun percentOf(basisPoints: Int) = Paise((value * basisPoints + 5_000) / 10_000)
        override fun toString() = "₹${value / 100}.${(value % 100).toString().padStart(2, '0')}"
    }
    
    fun split(total: Paise, parts: Int): List<Paise> {
        val base = total.value / parts
        val remainder = total.value % parts
        return List(parts) { i -> Paise(base + if (i < remainder) 1 else 0) }
    }
    
    fun main() {
        println(0.1 + 0.2 == 0.3)                   // false
    
        val subtotal = Paise(1999) * 3
        val tax = subtotal.percentOf(1800)
        println("$subtotal + $tax = ${subtotal + tax}") // ₹59.97 + ₹10.79 = ₹70.76
        println(split(Paise(1000), 3))              // [₹3.34, ₹3.33, ₹3.33]
    }
  10. Q70.How do you build and package a Kotlin service for deployment?

    Build the service with a reproducible build, produce a self-contained artifact, such as an executable archive with all dependencies or a distribution folder, and package it into a container image that layers dependencies separately from application code.

    Build practices:

    1. Pin the Kotlin version, the JVM version and dependency versions, and enable dependency verification or lock files.
    2. Use build caching and incremental compilation to keep builds fast.
    3. Run tests, static analysis and formatting checks as part of the build.
    4. Embed the version and commit identifier in the artifact for diagnostics.

    Container practices:

    • Layered images: dependencies change rarely, so put them in a lower layer and the application classes on top, which makes rebuilds and pulls fast.
    • A slim JVM runtime image, or a custom minimal runtime containing only the modules the service needs.
    • Non-root user and a read-only file system where possible.
    • Memory settings relative to the container limit, rather than fixed heap sizes.
    • Health checks and graceful shutdown handling.

    Optional native compilation produces a fast-starting executable without a JVM, at the cost of longer builds and restrictions on reflection.

  11. Q71.How do you reduce startup time and memory use for Kotlin services?

    Measure where startup time and memory go, then remove unnecessary work at startup, reduce reflection and consider ahead-of-time techniques when fast startup really matters, such as for autoscaling or scale-to-zero workloads.

    Standard JVM with just-in-time compilationAhead-of-time native image
    Slower startup and warm-up, then excellent peak performanceVery fast startup and low memory from the start
    Full support for reflection and dynamic featuresReflection and dynamic loading need configuration
    Simple buildsLong builds and some library limitations
    Best for long-running services with steady loadBest for short-lived tasks and dense, frequently scaled deployments

    Improvements on the standard JVM:

    1. Avoid heavy reflection at startup by using compile-time serialization and dependency injection that resolves at build time.
    2. Defer expensive initialization, such as warming caches, until after the service reports ready, if acceptable.
    3. Class data sharing and startup snapshots reduce class loading time.
    4. Right-size thread pools and caches, which affect baseline memory.
    5. Tune the heap relative to the container, and choose a collector suited to the heap size.

    Measure time to first successful request and steady-state memory under realistic load before and after each change.

  12. Q72.How do you size thread pools and connection pools for a Kotlin service?

    Size each pool from the work it does and the resource it protects. The number of operations in progress equals throughput times the time each operation takes, so a pool needs at least that many slots, plus headroom, but never more than the downstream resource can handle.

    Different pools, different rules:

    • CPU-bound work, such as Dispatchers.Default: about one thread per core; more threads only add switching.
    • Blocking calls, such as a blocking database driver: enough threads to cover concurrent calls, capped by the connection pool.
    • Database connection pools: limited by the database's connection capacity divided across all instances.
    • Suspending, non-blocking calls need few threads; concurrency is limited by connection pools and semaphores instead.

    Practical steps:

    1. Measure throughput and latency per operation under load.
    2. Compute needed concurrency, add headroom for spikes and cap it by downstream limits.
    3. Align limits: a parallelism limit on blocking database work should equal the connection pool size, so threads never wait for connections.
    4. Monitor pool usage, wait time and timeouts.

    Example

    A service runs 300 queries per second per instance, each taking 30 ms, and the database allows 200 connections across 8 instances.

    1. Concurrent queries needed: 300 × 0.030 = 9.
    2. With 50% headroom for spikes: about 14 connections per instance.
    3. Database budget: 200 / 8 = 25 connections per instance, so 14 fits, leaving room for migrations and administration.
    4. The blocking query work gets a dispatcher limited to 14 in parallel, matching the pool, so coroutines suspend rather than threads blocking on pool waits.
  13. Q73.How do you implement graceful shutdown for a Kotlin service?

    On a termination signal, stop accepting new requests, let in-flight requests and background coroutines finish within a time limit, then close resources such as connection pools and flush logs and metrics. Structured concurrency makes this clean: cancelling the application scope cancels every background coroutine, and their finally blocks run cleanup.

    sequenceDiagram
      participant P as Platform
      participant A as Application
      participant S as HTTP server
      participant B as Background scope
      P->>A: Termination signal
      A->>A: Fail readiness, wait briefly for load balancers
      A->>S: Stop accepting, finish in-flight requests within a grace period
      A->>B: Cancel scope and wait for children
      B-->>A: Finished, cleanup done in finally blocks
      A->>A: Close pools, flush telemetry
      A-->>P: Exit

    Implementation details:

    1. Grace period: shorter than the platform's kill timeout, so the process exits on its own.
    2. Background work: consumers stop taking new messages, finish or release current ones and commit offsets or acknowledgements.
    3. Suspending cleanup runs in withContext(NonCancellable) inside finally.
    4. Order: stop inputs first, then workers, then shared resources such as databases.
    5. Test it by sending termination signals under load and checking that no requests fail.
  14. Q74.How do you call other services reliably from Kotlin?

    Use a suspending HTTP client configured once per downstream service, with timeouts, connection pooling, limited retries and clear error handling, and wrap each service behind a small client class with typed results.

    flowchart LR
      A["Service code"] --> B["Typed client for the pricing service"]
      B --> C["Timeout per call"]
      C --> D["Retry transient failures with backoff"]
      D --> E["Concurrency limit and circuit breaker"]
      E --> F["Pooled HTTP connection"]
      F --> G["Pricing service"]

    Rules:

    1. Timeouts everywhere: connect, request and overall, within the caller's own deadline.
    2. Retries only for safe operations and transient errors, with backoff and jitter, never retrying cancellation.
    3. Circuit breaking: stop calling a failing dependency for a while and return a fallback, so failures do not cascade.
    4. Concurrency limits per dependency, with a semaphore.
    5. Typed errors: map responses to sealed results, such as NotFound or Unavailable, so callers handle each case.
    6. Propagate context: trace headers and request deadlines.
    7. Reuse clients: creating a client per call wastes connections and threads.

    Test clients against a local fake server that can return errors, slow responses and malformed bodies.

  15. Q75.How do you use Kotlin's type system to make illegal states unrepresentable?

    Design types so invalid combinations of data cannot be constructed. Instead of one class with many nullable fields and flags, model each valid state as its own type with exactly the fields it needs, and make transitions functions that only accept the right starting state.

    Techniques:

    1. Sealed hierarchies for states: a Shipped order always has a tracking code; a Pending order cannot have one.
    2. Value classes for validated primitives: an Email can only be created through a parser that validates it.
    3. Non-null types for required data, so missing values are impossible after validation.
    4. Private constructors with factories that enforce rules, such as a positive quantity.
    5. Transition functions that return the next state and reject invalid moves.

    Benefits:

    • Whole categories of checks disappear from business code, because the compiler guarantees them.
    • Reviews focus on transitions instead of scattered if checks.
    • Adding a state forces every when over the states to handle it.

    Balance: keep persistence and API models flexible where needed, and convert into strict domain types at the boundary.

    Example

    Each state carries only its own data, and an invalid transition is rejected.

    sealed interface Order {
        val id: String
    
        data class Pending(override val id: String) : Order
        data class Paid(override val id: String, val paymentId: String) : Order
        data class Shipped(override val id: String, val paymentId: String, val trackingCode: String) : Order
    }
    
    fun Order.pay(paymentId: String): Order = when (this) {
        is Order.Pending -> Order.Paid(id, paymentId)
        else -> error("cannot pay an order that is ${this::class.simpleName}")
    }
    
    fun Order.ship(trackingCode: String): Order = when (this) {
        is Order.Paid -> Order.Shipped(id, paymentId, trackingCode)
        else -> error("cannot ship an order that is ${this::class.simpleName}")
    }
    
    fun main() {
        println(Order.Pending("ord-7").pay("pay-1").ship("TRK-9"))
        try {
            Order.Pending("ord-8").ship("TRK-1")
        } catch (e: IllegalStateException) {
            println(e.message)
        }
    }
    // Output:
    // Shipped(id=ord-7, paymentId=pay-1, trackingCode=TRK-9)
    // cannot ship an order that is Pending
  16. Q76.A Kotlin service's latency jumps under load, but processor use stays moderate. What do you suspect?

    A blocking call is probably running on a dispatcher meant for non-blocking or CPU work, usually the default dispatcher. Its few threads are stuck waiting, so every coroutine scheduled on them waits too, and latency climbs even though the processor is mostly idle.

    How to confirm:

    1. Thread dumps show default dispatcher threads blocked in input and output, such as a synchronous database driver, a blocking HTTP client or Thread.sleep.
    2. Coroutine dumps show many coroutines suspended waiting to be dispatched.
    3. Latency correlates with traffic while processor use stays moderate.

    Fixes:

    • Wrap blocking calls in withContext(Dispatchers.IO), with a limited-parallelism view sized to the resource, such as the connection pool.
    • Replace blocking clients with suspending ones where possible.
    • Remove runBlocking from request paths.
    • Add a guard: debug tooling that detects blocking calls on dispatcher threads in tests and staging.

    Example

    The default dispatcher has 8 threads on an 8-core server, and a handler accidentally calls a blocking client that takes 200 ms.

    1. Each blocked call holds a thread for 200 ms, so 8 threads can complete at most 8 / 0.2 = 40 such requests per second.
    2. At 100 requests per second, 100 × 0.2 = 20 threads would be needed, so requests queue and latency grows without limit.
    3. Moving the call to the input and output dispatcher, limited to 32 in parallel, raises capacity to 32 / 0.2 = 160 requests per second.
    4. A suspending client removes the thread cost entirely, leaving the downstream service's capacity as the only limit.
  17. Q77.Memory in a Kotlin service grows steadily under constant traffic. What coroutine-related causes do you check?

    Besides ordinary leaks, such as unbounded caches, coroutine code has its own ways to hold memory: coroutines that never finish, buffers that grow without limit and replay caches that keep old values.

    Coroutine-related causes:

    1. Leaked coroutines: work launched in an application-wide scope that waits forever, such as on a channel nobody sends to, keeping its captured data alive.
    2. Unlimited channels: a producer faster than its consumer fills an unlimited buffer.
    3. Large replay caches on shared flows, holding many big events for late subscribers.
    4. Collectors that never stop, started per request but tied to a long-lived scope.
    5. Captured request data in long-lived lambdas, such as callbacks registered with a global registry.

    How to investigate:

    • Dump active coroutines with debugging tools and look for counts that grow.
    • Compare heap dumps and follow retention paths from coroutine continuation objects.
    • Check every launch that does not use the request's scope.

    Fixes: structured concurrency for per-request work, bounded channels with explicit overflow behavior, small replay values and timeouts on waits.

    Example

    A handler launches a follow-up coroutine in the application scope that waits for a confirmation event that never arrives for cancelled orders.

    1. About 2,000 cancelled orders occur per hour, and each waiting coroutine keeps roughly 50 KB of order data alive.
    2. Memory grows by about 2,000 × 50 KB = 100 MB per hour, until garbage collection runs constantly.
    3. A coroutine dump shows thousands of coroutines suspended at the same wait.
    4. Adding a timeout to the wait, and cancelling the waiter when the order is cancelled, keeps the number of waiting coroutines near zero.
  18. Q78.Cancelled requests in a Kotlin service keep doing work in the background. Why?

    Cancellation in coroutines is cooperative, so work continues when the code cannot observe the cancellation or actively suppresses it. The most common causes are swallowed cancellation exceptions, blocking calls and work started outside the request's scope.

    Typical causes:

    1. Swallowed cancellation: catch (e: Exception) or runCatching around suspending calls catches the cancellation exception; if it is not rethrown, the coroutine continues as if nothing happened.
    2. Blocking or CPU loops that never reach a suspension point or check isActive.
    3. Unscoped launches: coroutines started in a global or application scope do not belong to the request, so cancelling the request does not reach them.
    4. NonCancellable used too broadly, instead of only for short cleanup.
    5. Callback wrappers that do not cancel the underlying operation when the coroutine is cancelled.

    Fixes:

    • Catch specific exceptions, or rethrow cancellation explicitly in generic handlers.
    • Add ensureActive() in long loops, and use interruptible or suspending APIs.
    • Start child work in coroutineScope within the request.
    • Register cancellation handlers in callback wrappers.

    Test it: cancel a running operation in a test and assert that no further side effects happen.

  19. Q79.A Kotlin service throws a null pointer exception even though the code uses null safety. How can that happen?

    Kotlin's null safety is checked at compile time, but some values bypass the checks at run time. The usual sources are platform types from JVM libraries, uninitialized lateinit properties, explicit !! assertions and deserialization or reflection that creates objects without running the normal constructor checks.

    Where to look:

    1. Platform types: a library method without nullability annotations returns null, and the value is assigned to a non-null Kotlin type, which throws at that point.
    2. !! on a value the developer assumed could not be null.
    3. lateinit properties read before initialization, which throw a specific uninitialized-property exception.
    4. Deserialization that bypasses constructors and leaves non-null fields null.
    5. Initialization order: calling an overridable method from a base class constructor, which reads a subclass property before it is set.

    Prevention:

    • Declare explicit Kotlin types at interop boundaries, choosing nullable types when unsure.
    • Search for and justify every !!.
    • Prefer constructor parameters over lateinit.
    • Configure serialization to respect nullability, and validate input into domain types.

    Example

    An uninitialized lateinit property and a null platform value both fail at run time despite non-null declarations.

    class ReportService {
        lateinit var template: String
        fun render(name: String) = template.replace("{name}", name)
    }
    
    fun main() {
        val service = ReportService()
        try {
            service.render("q1")
        } catch (e: UninitializedPropertyAccessException) {
            println("not initialized: ${e.message}")
        }
        service.template = "Report for {name}"
        println(service.render("q1"))
    
        try {
            val value: String = System.getenv("SURELY_NOT_SET_42")
            println(value.length)
        } catch (e: NullPointerException) {
            println("platform value was null")
        }
    }
    // Output:
    // not initialized: lateinit property template has not been initialized
    // Report for q1
    // platform value was null
  20. Q80.A message consumer written in Kotlin sometimes processes the same message twice. How do you fix it?

    Most message brokers deliver at least once: after a crash, a timeout or a rebalance, a message can be delivered again. The consumer must therefore be idempotent, recording each processed message ID and skipping messages it has already handled.

    Making a consumer idempotent:

    1. Use a stable message ID, set by the producer, not generated on receipt.
    2. Record processed IDs in the same database transaction as the business change, with a unique constraint, so the check and the write are atomic.
    3. Acknowledge after processing succeeds and commits.
    4. Design operations to be naturally idempotent where possible, such as setting a status rather than incrementing a counter.
    5. Expire old IDs after a period longer than the broker's maximum redelivery window.

    Related issues:

    • Ordering: if messages can arrive out of order, compare versions or timestamps before applying updates.
    • Poison messages: messages that always fail should go to a dead-letter queue after a few attempts.
    • Partial side effects: external calls, such as emails, need their own idempotency keys.

    Example

    A redelivered message is detected by its ID, so the ledger records the payment once.

    data class Message(val id: String, val orderId: String, val amountMinor: Long)
    
    class PaymentConsumer {
        private val processed = mutableSetOf<String>()
        val ledger = mutableListOf<String>()
    
        fun handle(message: Message): String {
            if (!processed.add(message.id)) return "skipped duplicate ${message.id}"
            ledger += "${message.orderId}:${message.amountMinor}"
            return "recorded ${message.id}"
        }
    }
    
    fun main() {
        val consumer = PaymentConsumer()
        listOf(Message("m1", "ord-1", 45_000), Message("m2", "ord-2", 9_900), Message("m1", "ord-1", 45_000))
            .forEach { println(consumer.handle(it)) }
        println(consumer.ledger)
    }
    // Output:
    // recorded m1
    // recorded m2
    // skipped duplicate m1
    // [ord-1:45000, ord-2:9900]

    In production, the processed IDs live in a database table with a unique constraint, written in the same transaction as the ledger entry.