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 1-20 of 100

  1. Q1.What does a Kotlin backend developer own beyond writing API endpoints?

    A Kotlin backend developer owns services from design to production: their APIs, domain model, concurrency, data access, reliability and operations. Endpoints are a thin layer; most of the work is in modeling the domain safely and making the service behave well under load.

    Typical responsibilities:

    • Domain modeling: data classes, sealed hierarchies and value classes that make invalid states hard to represent.
    • Concurrency: coroutines with structured concurrency, cancellation and timeouts.
    • API design: request validation, consistent errors and versioning.
    • Data access: queries, transactions, connection pools and caching.
    • Reliability: retries, circuit breaking, graceful shutdown and health checks.
    • Performance: profiling on the JVM, tuning memory and avoiding blocking calls in coroutine code.
    • Testing: fast unit tests with fakes, integration tests and tests for coroutine code with virtual time.
    • Delivery: builds, container images and safe deployments.

    Kotlin's strength is expressing intent concisely and safely, so a strong developer uses the type system to prevent bugs rather than relying only on tests to find them.

  2. Q2.Why do teams choose Kotlin for backend services?

    Kotlin combines a concise, safe language with the mature JVM runtime and its huge library ecosystem. Teams get modern language features without giving up proven performance, tooling and operational knowledge.

    Main strengths:

    • Null safety in the type system, which removes a large class of runtime errors.
    • Concise code: data classes, type inference, extension functions and expressive collection operations reduce boilerplate.
    • Coroutines: asynchronous code written in a sequential style, with structured concurrency and cancellation.
    • Interoperability: existing JVM libraries and frameworks can be used directly, and codebases can migrate file by file.
    • Expressive domain modeling: sealed hierarchies and when expressions make business rules explicit and checked by the compiler.
    • Multiplatform options for sharing code, such as validation rules, with other targets.

    Trade-offs:

    1. Builds can be slower than for some other languages, especially with compiler plugins.
    2. Powerful features, such as extension functions and DSLs, can make code clever but hard to read if overused.
    3. Interop with libraries that lack nullability information brings back some null risks.
  3. Q3.How does null safety work in Kotlin?

    Kotlin's type system separates references that can hold null from those that cannot. String can never be null, while String? can, and the compiler forces you to handle the null case before using a nullable value.

    Tools for nullable values:

    • Safe call ?.: customer?.address?.city returns null if any part is null.
    • Elvis operator ?:: supplies a default or exits early, as in val id = raw ?: return badRequest().
    • Smart casts: after if (name != null), the compiler treats name as non-null.
    • let: runs a block only for non-null values, user?.let { send(it) }.
    • Not-null assertion !!: throws if the value is null; use it rarely, since it discards the safety.

    Where nulls still appear:

    1. Platform types from JVM libraries without nullability annotations, which the compiler cannot check.
    2. Deserialized data that is missing fields despite non-null types, depending on the serialization library.
    3. Late initialization with lateinit, which throws if read before being set.

    Good practice: convert untrusted data into non-null domain types at the boundary, so the rest of the code works with guaranteed values.

    Example

    Safe calls and the Elvis operator handle every missing piece; !! throws when its assumption is wrong.

    data class Address(val city: String?)
    data class Customer(val name: String, val address: Address?)
    
    fun cityOf(customer: Customer?): String = customer?.address?.city ?: "unknown"
    
    fun main() {
        println(cityOf(Customer("c1", Address("Pune")))) // Pune
        println(cityOf(Customer("c2", null)))            // unknown
        println(cityOf(null))                            // unknown
    
        val raw: String? = "42"
        println((raw?.toIntOrNull() ?: 0) + 1)           // 43
    
        val missing: String? = null
        try {
            missing!!.length
        } catch (e: NullPointerException) {
            println("!! threw on null")                  // !! threw on null
        }
    }
  4. Q4.What is the difference between `val` and `var`, and what does immutability mean in Kotlin?

    val declares a reference that cannot be reassigned; var declares one that can. Neither says anything about whether the object itself can change: a val holding a mutable list can still have items added.

    Levels of immutability:

    • Read-only references: val prevents reassignment of the variable.
    • Read-only interfaces: List, Set and Map have no mutating methods, but the underlying collection may still be mutable through another reference, such as a MutableList.
    • Truly immutable data: data classes with only val properties of immutable types, and defensive copies of collections, such as toList().

    Good practices:

    1. Prefer val everywhere; use var only for local state that genuinely changes, such as a loop accumulator.
    2. Expose read-only types from classes, keeping mutable collections private.
    3. Copy on input when storing collections passed in by callers, so later changes by the caller do not leak in.
    4. Use copy on data classes to create modified versions instead of mutating shared objects.

    Immutability makes code easier to reason about and safe to share between coroutines and threads without locks.

  5. Q5.How do data classes work in Kotlin?

    A data class is a class whose main purpose is holding data. The compiler generates equals, hashCode, toString, copy and component functions for destructuring, all based on the properties declared in the primary constructor.

    What you get:

    • Value equality: two instances with equal properties are equal, so data classes work well as map keys and in sets.
    • Readable toString: Order(id=7, status=paid, total=1250).
    • copy: creates a modified copy, such as order.copy(status = "shipped"), leaving the original unchanged.
    • Destructuring: val (id, status) = order.

    Rules and cautions:

    1. Only properties in the primary constructor take part in the generated methods.
    2. Keep properties val; mutable data classes used as map keys break when they change.
    3. copy is shallow: a copied data class shares the same list objects.
    4. Destructuring depends on property order, so reordering constructor parameters silently changes destructured values.
    5. Data classes cannot be abstract or sealed themselves, but they can implement sealed interfaces.

    Typical uses: request and response models, events, query results and value objects.

    Example

    Equality, readable printing, copying and destructuring all come from the constructor's properties.

    data class Order(val id: Int, val status: String, val total: Long)
    
    fun main() {
        val a = Order(7, "paid", 1250)
        val b = Order(7, "paid", 1250)
    
        println(a == b)                 // true
        println(a)                      // Order(id=7, status=paid, total=1250)
        println(setOf(a, b).size)       // 1
    
        val shipped = a.copy(status = "shipped")
        val (id, status) = shipped
        println("$id $status ${a.status}") // 7 shipped paid
    }
  6. Q6.How do sealed classes and sealed interfaces model closed hierarchies?

    A sealed class or interface restricts which types can extend it: all direct subtypes must be declared in the same module and package. The compiler therefore knows every possible case and can check that a when expression handles them all.

    Why they are useful in backend code:

    • Results with several outcomes: success, validation failure, not found and conflict, each with its own data.
    • Domain states: an order that is pending, paid, shipped or cancelled, where each state carries different fields.
    • Events and commands in message-driven systems.

    Benefits:

    1. Exhaustive when: adding a new case makes the compiler flag every when that does not handle it.
    2. Type-specific data: each subtype holds exactly the fields that make sense for it, unlike a single class with many nullable fields.
    3. Explicit error handling: failures become values in the type signature rather than hidden exceptions.

    Choosing a form:

    • sealed interface when subtypes might also implement other interfaces.
    • data class subtypes for cases with data, and data object for cases without.

    Example

    Each payment outcome carries its own data, and the when must cover every case.

    sealed interface PaymentResult {
        data class Success(val transactionId: String) : PaymentResult
        data class Declined(val reason: String) : PaymentResult
        data object Timeout : PaymentResult
    }
    
    fun message(result: PaymentResult): String = when (result) {
        is PaymentResult.Success -> "paid: ${result.transactionId}"
        is PaymentResult.Declined -> "declined: ${result.reason}"
        PaymentResult.Timeout -> "try again later"
    }
    
    fun main() {
        listOf(PaymentResult.Success("tx-1"), PaymentResult.Declined("insufficient funds"), PaymentResult.Timeout)
            .forEach { println(message(it)) }
    }
    // Output:
    // paid: tx-1
    // declined: insufficient funds
    // try again later
  7. Q7.When do you use an enum class and when a sealed hierarchy in Kotlin?

    Both describe a closed set of possibilities that the compiler can check exhaustively. An enum has a fixed set of single instances with the same shape; a sealed hierarchy has subtypes that can each carry different data and have many instances.

    Enum classSealed hierarchy
    A fixed list of constant instances, such as PAID and SHIPPEDA fixed list of subtypes, each possibly with its own fields
    Every constant has the same propertiesEach subtype can hold different data
    Built-in entries, valueOf and stable names for storageInstances created freely with their own values
    Best for simple states, categories and codesBest for results, events and states carrying data

    Practical guidance:

    1. Store enums by an explicit code property, not by ordinal position, so reordering constants does not corrupt stored data.
    2. Parse safely with entries.firstOrNull { it.code == raw } rather than valueOf, which throws on unknown values.
    3. Move to a sealed hierarchy when one case starts needing extra data, such as a cancellation reason.
    4. Put behavior on the type: enums and sealed types can have methods, such as allowed state transitions.
  8. Q8.How do `when` expressions and smart casts work in Kotlin?

    when is an expression that picks the first matching branch and returns its value. It can match constants, ranges, types and arbitrary conditions. After a type check such as is String, the compiler smart-casts the value, so the branch can use it as that type without an explicit cast.

    Forms of when:

    • With a subject: when (status) { "paid" -> ...; "shipped", "delivered" -> ...; else -> ... }.
    • Type checks: is Int -> with a smart cast inside the branch.
    • Ranges: in 1..10 ->.
    • Without a subject: each branch is a Boolean condition, replacing long if-else chains.

    Exhaustiveness: when the subject is an enum, a sealed type or a Boolean, the compiler requires every case to be covered if when is used as an expression; this turns new cases into compile errors rather than runtime surprises.

    Smart cast limits: they work only when the compiler can prove the value has not changed since the check, so they apply to local vals and immutable properties, not to var properties that another thread could modify.

    Example

    Type checks smart-cast the value inside each branch, and a subject-less when replaces an if chain.

    fun describe(x: Any?): String = when (x) {
        null -> "nothing"
        is Int -> if (x > 100) "large number" else "number $x"
        is String -> "text of ${x.length} chars"
        is List<*> -> "list of ${x.size}"
        else -> "other"
    }
    
    fun shippingBand(kg: Double): String = when {
        kg <= 1 -> "light"
        kg <= 10 -> "standard"
        else -> "heavy"
    }
    
    fun main() {
        listOf(null, 7, 500, "hello", listOf(1, 2), 2.5).forEach { println(describe(it)) }
        println(shippingBand(4.5))
    }
    // Output:
    // nothing
    // number 7
    // large number
    // text of 5 chars
    // list of 2
    // other
    // standard
  9. Q9.How do extension functions work in Kotlin, and when should you use them?

    An extension function adds a function to an existing type without changing or inheriting from it, such as fun String.toSlug(). The compiler turns it into a static function that takes the receiver as its first parameter, so it can only use the type's public members.

    What extensions are good for:

    • Readable helpers on types you do not own, such as strings, collections and library types.
    • Domain conversions, such as fun OrderEntity.toResponse(), keeping mapping code out of the entity.
    • Scoped utilities: extensions declared inside a class or file are visible only there.
    • Extension properties, such as val Long.asAmount: String get() = ..., for computed values.

    Rules to remember:

    1. Extensions are resolved statically, by the declared type, not the runtime type, so they do not override member functions.
    2. A member function always wins over an extension with the same signature.
    3. Nullable receivers are allowed, as in fun String?.orUnknown().

    Avoid: large numbers of extensions on very common types, such as Any or String, which clutter autocompletion, and extensions that hide expensive work behind what looks like a simple property.

    Example

    Extensions add a slug helper to String, a formatting property to Long and a safe accessor to lists.

    fun String.toSlug(): String =
        lowercase().trim().replace(Regex("""[^\p{L}\p{N}]+"""), "-").trim('-')
    
    val Long.asAmount: String
        get() = "${this / 100}.${(this % 100).toString().padStart(2, '0')}"
    
    fun <T> List<T>.secondOrNull(): T? = getOrNull(1)
    
    fun main() {
        println("Senior Backend Developer (Remote)!".toSlug()) // senior-backend-developer-remote
        println(125050L.asAmount)                            // 1250.50
        println(listOf("a").secondOrNull())                  // null
    }
  10. Q10.How do higher-order functions and lambdas work in Kotlin?

    A higher-order function takes functions as parameters or returns them. Lambdas, written { x -> x * 2 }, are function values that can capture variables from their surrounding scope. Function types, such as (Long) -> Long, describe them precisely.

    Syntax features:

    • Trailing lambdas: orders.filter { it.total > 1000 } puts the lambda outside the parentheses.
    • it names the single parameter of a lambda when not declared.
    • Function references: ::isValid or String::length pass existing functions.
    • Lambdas with receivers: StringBuilder.() -> Unit lets the lambda call the receiver's members directly, which is how builders and DSLs work.

    Closures: lambdas can read and modify captured local variables. This is convenient, but shared mutable captures are not thread-safe when lambdas run on other threads or coroutines.

    Performance: non-inline higher-order functions allocate a function object for each capturing lambda. Standard collection functions are inline, so their lambdas cost nothing extra; mark your own small higher-order functions inline when they are on hot paths.

    Uses in backend code: collection processing, retry and transaction helpers that take a block, configuration builders and strategies passed into services.

  11. Q11.What do inline functions and reified type parameters do in Kotlin?

    An inline function's body, including the lambdas passed to it, is copied into each call site by the compiler. This removes the cost of allocating function objects for lambdas and allows two special features: non-local returns and reified type parameters.

    Non-local returns: inside a lambda passed to an inline function, return exits the enclosing function, which makes code like forEach { if (it < 0) return } behave like a loop.

    Reified type parameters: normally, generic types are erased at run time, so T cannot be checked. In an inline function, reified T keeps the type, so you can write it is T, T::class or filterIsInstance<T>().

    When to use inline:

    1. Small higher-order functions called frequently, such as timing, locking or transaction helpers.
    2. Functions that need reified types, such as typed parsing or lookups.

    When not to:

    • Large functions, since inlining copies the body everywhere and increases code size.
    • Functions without lambda parameters or reified types, where inlining gains nothing.

    crossinline and noinline modifiers control lambdas that must not use non-local returns or must be stored.

    Example

    A reified type parameter filters by type at run time, and a non-local return exits the outer function from inside forEach.

    inline fun <reified T> List<Any>.ofType(): List<T> = filter { it is T }.map { it as T }
    
    fun firstPositive(values: List<Int>): Int? {
        values.forEach { if (it > 0) return it }
        return null
    }
    
    fun main() {
        val mixed: List<Any> = listOf(1, "a", 2.5, "b", 3)
        println(mixed.ofType<String>())          // [a, b]
        println(mixed.ofType<Int>().sum())       // 4
        println(firstPositive(listOf(-1, 0, 3, 5))) // 3
    }
  12. Q12.What do the scope functions `let`, `run`, `with`, `apply` and `also` do?

    Scope functions run a block of code in the context of an object. They differ in how the block refers to the object, as this or it, and in what they return, the object itself or the block's result.

    letrunwith
    Object is it; returns the block's resultObject is this; returns the block's resultObject is this, passed as an argument; returns the block's result
    Null checks: user?.let { send(it) }Compute a value from an object's membersGroup several calls on one object
    applyalso
    Object is this; returns the objectObject is it; returns the object
    Configure a new object: Config().apply { port = 8080 }Side effects in a chain, such as logging

    Guidelines:

    1. Pick by intent: apply to configure, let for nullable values and transformations, also for side effects, run and with to compute results.
    2. Avoid nesting scope functions; nested it and this become confusing quickly.
    3. Do not use them just to save a line; a plain local variable is often clearer.

    Example

    Each scope function used for its typical purpose.

    data class Config(var host: String = "", var port: Int = 0)
    
    fun main() {
        val config = Config().apply {
            host = "db.internal"
            port = 6000
        }
        val address = with(config) { "$host:$port" }
        val length = "order-42".let { it.length }
        val user: String? = "u7"
        user?.let { println("loading $it") }
        val ids = mutableListOf(1, 2).also { println("created with ${it.size} items") }
        val total = ids.run { sum() * 10 }
    
        println("$address $length $total")
    }
    // Output:
    // loading u7
    // created with 2 items
    // db.internal:6000 8 30
  13. Q13.How do read-only and mutable collections differ in Kotlin, and which operations matter most?

    Kotlin separates collection interfaces into read-only ones, List, Set and Map, and mutable ones, MutableList, MutableSet and MutableMap. Functions that only read data accept the read-only types, which documents intent and prevents accidental changes.

    Read-only interfacesMutable interfaces
    List<T>, Set<T>, Map<K, V>MutableList<T>, MutableSet<T>, MutableMap<K, V>
    Expose only reading operationsAdd add, remove, put and similar operations
    Created with listOf, setOf, mapOfCreated with mutableListOf, mutableSetOf, mutableMapOf
    Best for parameters, return values and shared dataBest for local building, then exposed as read-only

    Read-only is not immutable: a List reference may point to a list that someone else modifies; copy with toList() when you need a stable snapshot.

    Operations worth knowing:

    • map, filter, flatMap and fold for transformations.
    • groupBy, associateBy and partition for organizing data.
    • sumOf, maxByOrNull and count for aggregation.
    • windowed and chunked for batches and sliding windows.
    • buildList and buildMap to construct a collection with mutable operations, then get a read-only result.

    Example

    Grouping, partitioning and aggregation on a read-only list of orders.

    data class Order(val region: String, val total: Long)
    
    fun main() {
        val orders = listOf(Order("north", 900), Order("south", 7400), Order("north", 7650))
    
        println(orders.groupBy { it.region }.mapValues { (_, list) -> list.sumOf { it.total } }) // {north=8550, south=7400}
        val (large, small) = orders.partition { it.total > 1000 }
        println("${large.size} large, ${small.size} small")                                   // 2 large, 1 small
        println(orders.maxBy { it.total }.region)                                              // north
        println(orders.map { it.total }.chunked(2))                                            // [[900, 7400], [7650]]
    }
  14. Q14.How do sequences differ from collections in Kotlin?

    Collection operations are eager: each step, such as map or filter, processes every element and builds a new intermediate list. Sequence operations are lazy: elements flow through the whole chain one at a time, and processing stops as soon as the result is known.

    CollectionsSequences
    Each operation runs over all elements and creates a new listElements pass through all operations one at a time
    Fast for small and medium data with few stepsBetter for long chains, large data or early termination
    Results are ready immediatelyNothing runs until a terminal operation, such as toList or first
    Can be iterated many timesSome sequences can be iterated only once

    When to use sequences:

    1. Large inputs with several chained steps, to avoid big intermediate lists.
    2. Searches that can stop early, such as first { ... }.
    3. Generated or infinite data, with generateSequence or the sequence { yield(...) } builder.
    4. Reading lines from large files lazily.

    When not to: small collections, where the overhead of laziness outweighs the benefit, and code that needs random access or sizes.

    Example

    The lazy version stops after six elements; the eager version maps all thousand before searching.

    fun main() {
        var lazyCount = 0
        val lazyResult = (1..1_000).asSequence().map { lazyCount++; it * 2 }.first { it > 10 }
        println("$lazyResult after $lazyCount mappings")   // 12 after 6 mappings
    
        var eagerCount = 0
        val eagerResult = (1..1_000).map { eagerCount++; it * 2 }.first { it > 10 }
        println("$eagerResult after $eagerCount mappings") // 12 after 1000 mappings
    }
  15. Q15.How do generics and variance work in Kotlin?

    Generics let classes and functions work with many types safely. Variance describes how subtyping of type arguments relates to subtyping of the generic type: whether a List<Int> can be used where a List<Number> is expected.

    Declaration-site variance:

    • out T (covariant): the type only produces T, so Source<Int> can be used as Source<Number>. Kotlin's read-only List<out E> works this way.
    • in T (contravariant): the type only consumes T, so a Comparator<Any> can be used as a Comparator<String>.
    • Invariant by default: MutableList<Int> is not a MutableList<Number>, because adding a Double would break it.

    Use-site variance: fun copy(from: Array<out Number>) restricts a parameter at the point of use when the declaration is invariant.

    Star projection: List<*> means a list of some unknown type; you can read elements as Any? but cannot add any.

    Practical rules:

    1. Mark interfaces that only return T as out, and those that only accept T as in.
    2. Use reified type parameters in inline functions when you need the type at run time.
    3. Keep generic signatures simple; complex bounds make APIs hard to use.
  16. Q16.How do object declarations and companion objects work in Kotlin?

    An object declaration defines a class and its single instance at once, created lazily and thread-safely on first access. A companion object is such an object nested in a class, holding members that belong to the class rather than to instances, similar to static members in other languages.

    Typical uses of object:

    • Stateless utilities and constants.
    • Implementations of interfaces that need only one instance, such as a comparator.
    • data object for cases without data in sealed hierarchies.

    Typical uses of companion object:

    1. Factory functions with names, such as Money.of("12.50"), often alongside a private constructor that enforces validation.
    2. Constants related to the class, with const val for compile-time values.
    3. Implementing interfaces on the companion, so the class itself can act as a factory.

    Cautions for backend code:

    • Singletons with mutable state are shared across every request and coroutine, so they need synchronization and are hard to replace in tests.
    • Prefer dependency injection for anything with side effects, such as clients or repositories, and keep object for pure, stateless code.
  17. Q17.How do class delegation and delegated properties work in Kotlin?

    Delegation hands work to another object automatically. Class delegation, class LoggingRepo(inner: Repo) : Repo by inner, implements an interface by forwarding every method to inner, so you override only the methods you want to change. Delegated properties, val x by something, move a property's getter and setter logic into a reusable delegate.

    Built-in property delegates:

    • lazy: computes the value on first access and caches it, thread-safely by default.
    • Delegates.observable: runs a callback after each change.
    • Delegates.vetoable: can reject a change.
    • Map delegation: val name: String by map reads the value from a map, useful for dynamic data.

    Why class delegation is useful: it is the decorator pattern without boilerplate, such as adding logging, caching or metrics around a repository interface while keeping the original implementation untouched.

    Cautions:

    1. lazy values that throw during initialization will retry on the next access; make initialization safe or handle the failure.
    2. Delegation forwards to a fixed instance chosen at construction.

    Example

    A lazy property computes once, an observable property reports changes and class delegation decorates one method.

    import kotlin.properties.Delegates
    
    interface Notifier {
        fun send(message: String): String
        fun channel(): String
    }
    
    class EmailNotifier : Notifier {
        override fun send(message: String) = "email: $message"
        override fun channel() = "email"
    }
    
    class LoggingNotifier(private val inner: Notifier) : Notifier by inner {
        override fun send(message: String) = "[logged] " + inner.send(message)
    }
    
    class Connection {
        val handle by lazy { println("connecting"); "conn-1" }
        var status by Delegates.observable("new") { _, old, new -> println("status $old -> $new") }
    }
    
    fun main() {
        val c = Connection()
        println(c.handle)
        println(c.handle)
        c.status = "open"
    
        val notifier = LoggingNotifier(EmailNotifier())
        println(notifier.send("hi") + " via " + notifier.channel())
    }
    // Output:
    // connecting
    // conn-1
    // conn-1
    // status new -> open
    // [logged] email: hi via email
  18. Q18.How do value classes help with type safety and performance in Kotlin?

    A value class wraps a single value, such as an ID or an email address, in its own type. At run time, the compiler usually represents it as the underlying value, so you get distinct types without the cost of allocating wrapper objects.

    Benefits:

    • No mix-ups: a function taking OrderId cannot be called with a CustomerId, even though both wrap strings.
    • Validation in one place: a factory function parses and validates the raw value, so every instance is valid.
    • Readable signatures: fun refund(order: OrderId, amount: Money) documents itself.
    • Little overhead in most uses, since no extra object is created.

    Limits:

    1. A value class holds exactly one property.
    2. It is boxed into a real object in some situations, such as when used as a generic type argument or a nullable type.
    3. Equality is based on the wrapped value.
    4. Serialization and database libraries may need configuration to handle them.

    Good candidates: identifiers, email addresses, currency codes, percentages and amounts in minor units.

    Example

    Distinct ID types prevent mix-ups, and a private constructor forces validation through a factory.

    @JvmInline
    value class OrderId(val value: String)
    
    @JvmInline
    value class Email private constructor(val value: String) {
        companion object {
            fun parse(raw: String): Email? = raw.trim().lowercase().takeIf { "@" in it }?.let(::Email)
        }
    }
    
    fun load(order: OrderId): String = "loading ${order.value}"
    
    fun main() {
        println(load(OrderId("ord-7")))                     // loading ord-7
        println(Email.parse("  Buyer@Example.com ")?.value) // buyer@example.com
        println(Email.parse("not-an-email"))                // null
    }

    Calling load with any other type, such as a plain string or a customer ID class, does not compile.

  19. Q19.How does error handling work in Kotlin without checked exceptions?

    Kotlin has no checked exceptions: the compiler never forces callers to catch or declare them. Errors are handled either with exceptions for unexpected failures, or with return types that make expected failures explicit, such as nullable results, sealed result types or the standard Result type.

    Choosing an approach:

    • Exceptions for programming errors and failures the caller cannot handle, such as a broken database connection.
    • Nullable returns for simple "not found" cases, such as findById returning Order?.
    • Sealed result types for business outcomes with several failure kinds, such as validation errors and conflicts.
    • Result and runCatching to capture exceptions as values, with map, getOrElse and onFailure.

    Helpers for validation: require throws IllegalArgumentException for bad arguments, check throws IllegalStateException for bad state and error("message") throws immediately.

    Caution with coroutines: runCatching also catches cancellation exceptions, which breaks cooperative cancellation; rethrow them or catch narrower exception types in suspending code.

    At API boundaries, convert exceptions and failure results into consistent error responses, and never expose stack traces.

    Example

    runCatching turns parsing failures into values, require adds a business rule and getOrElse supplies a default.

    fun parsePort(raw: String): Result<Int> =
        runCatching { raw.trim().toInt() }
            .mapCatching { port -> require(port in 1..65535) { "port out of range: $port" }; port }
    
    fun main() {
        println(parsePort("8080").getOrNull())                                // 8080
        println(parsePort("eighty").exceptionOrNull()!!::class.simpleName)    // NumberFormatException
        println(parsePort("70000").exceptionOrNull()?.message)                // port out of range: 70000
        println(parsePort("abc").getOrElse { 80 })                            // 80
    }
  20. Q20.How do operator overloading and conventions work in Kotlin?

    Kotlin maps operators to functions with fixed names marked operator. Writing a + b calls a.plus(b), a[i] calls get(i), a > b calls compareTo and x in range calls contains. Defining these functions lets your own types use natural syntax.

    Common conventions:

    • Arithmetic: plus, minus, times, div, rem and unary operators.
    • Comparison: compareTo for <, >, <= and >=.
    • Indexing: get and set for [].
    • Membership and ranges: contains for in, and rangeTo for ...
    • Invocation: invoke lets an object be called like a function.
    • Destructuring: component1, component2 and so on.

    Good uses:

    1. Value types such as money, durations and vectors, where + and comparisons have obvious meaning.
    2. Domain collections, such as a cart with cart[sku].

    Cautions: overload operators only when the meaning is unsurprising. + on money should refuse to add different currencies rather than silently convert, and operators should not have side effects on their operands.