androidengineers.Book a session
← All interview questions
KotlinIntermediate4 min

Builder Pattern in Kotlin?

Answer

Builder Pattern in Kotlin = “construct a complex object step-by-step, without a 12-parameter constructor turning into a crime scene.”

In interviews, they’re checking 3 things:

  1. you know why builders exist
  2. you can implement one in Kotlin idiomatically
  3. you know when not to use it (because Kotlin has better tools)

What is Builder Pattern?

A Builder separates how you build an object from the object itself.

Use it when:

  • Object has many optional parameters
  • There are validation rules
  • You want readable, named configuration
  • Construction is multi-step (e.g., build request → add headers → add body → validate → create)

Classic Java-style builders were created to avoid:

new User("Akshay", null, null, null, 0, false, null, ...)

Kotlin’s “default args” vs Builder (important interview point)

Kotlin often removes the need for builders because we can do:

data class User(
  val name: String,
  val age: Int = 0,
  val city: String? = null,
  val isPro: Boolean = false
)

So: Builder is NOT always needed in Kotlin.

But builder still wins when:

  • You must enforce rules (e.g. if isPro == true then subscriptionId must exist)
  • You want a fluent DSL for configuration
  • You’re creating immutable objects with many “optional but not really optional” fields
  • You are interacting with Java libs / APIs that expect builder style

1) Classic Builder in Kotlin (Interview-Friendly)

Scenario: building a NetworkRequest

You want:

  • required: url
  • optional: method, headers, body, timeout
  • validation: GET can’t have body, timeout must be positive
class NetworkRequest private constructor(
    val url: String,
    val method: String,
    val headers: Map<String, String>,
    val body: String?,
    val timeoutMs: Long
) {
    class Builder(private val url: String) {
        private var method: String = "GET"
        private val headers: MutableMap<String, String> = mutableMapOf()
        private var body: String? = null
        private var timeoutMs: Long = 10_000

        fun method(value: String) = apply {
            method = value.uppercase()
        }

        fun addHeader(key: String, value: String) = apply {
            headers[key] = value
        }

        fun body(value: String?) = apply {
            body = value
        }

        fun timeoutMs(value: Long) = apply {
            timeoutMs = value
        }

        fun build(): NetworkRequest {
            require(url.startsWith("http")) { "Invalid url: $url" }
            require(timeoutMs > 0) { "Timeout must be > 0" }
            if (method == "GET") require(body == null) { "GET request can't have a body" }

            return NetworkRequest(
                url = url,
                method = method,
                headers = headers.toMap(), // immutable copy
                body = body,
                timeoutMs = timeoutMs
            )
        }
    }
}

Usage:

val request = NetworkRequest.Builder("https://api.masterly.app/sessions")
    .method("POST")
    .addHeader("Authorization", "Bearer token")
    .addHeader("Content-Type", "application/json")
    .body("""{"skillId":"compose","minutes":30}""")
    .timeoutMs(15_000)
    .build()

Interview points to say out loud:

  • private constructor forces creation only via Builder (immutability control)
  • apply returns the builder itself → fluent chain
  • build() is where validation lives
  • immutable copy of headers prevents accidental mutation after build

2) Kotlin DSL Builder (Idiomatic + Sexy)

Kotlin lets you write builders that feel like config blocks.

class NetworkRequest private constructor(
    val url: String,
    val method: String,
    val headers: Map<String, String>,
    val body: String?,
    val timeoutMs: Long
) {
    class Builder {
        lateinit var url: String
        var method: String = "GET"
        var body: String? = null
        var timeoutMs: Long = 10_000
        private val headers: MutableMap<String, String> = mutableMapOf()

        fun header(key: String, value: String) {
            headers[key] = value
        }

        fun build(): NetworkRequest {
            check(::url.isInitialized) { "url is required" }
            require(timeoutMs > 0) { "Timeout must be > 0" }
            val m = method.uppercase()
            if (m == "GET") require(body == null) { "GET request can't have a body" }

            return NetworkRequest(
                url = url,
                method = m,
                headers = headers.toMap(),
                body = body,
                timeoutMs = timeoutMs
            )
        }
    }
}

fun networkRequest(block: NetworkRequest.Builder.() -> Unit): NetworkRequest {
    return NetworkRequest.Builder().apply(block).build()
}

Usage:

val request = networkRequest {
    url = "https://api.masterly.app/sessions"
    method = "POST"
    header("Authorization", "Bearer token")
    header("Content-Type", "application/json")
    body = """{"skillId":"compose","minutes":30}"""
    timeoutMs = 15_000
}

Interview one-liner: “This is Builder Pattern using Kotlin DSL: the builder is configured via a lambda with receiver.”


3) Builder vs copy() (data class style)

For immutable data classes, copy() is often the “micro-builder”:

data class User(
    val name: String,
    val city: String? = null,
    val age: Int = 0
)

val u1 = User(name = "Akshay")
val u2 = u1.copy(city = "Bangalore", age = 26)

So you say: “If it’s just optional params and immutability, Kotlin default args + data class copy() often beats builders.”


Real Android examples you can mention in interview

  • OkHttp: Request.Builder(), OkHttpClient.Builder()
  • NotificationCompat.Builder
  • AlertDialog.Builder
  • WorkManager: OneTimeWorkRequestBuilder, constraints builder-ish patterns

These are builders because configuration has many options + readable setup.


Common interview questions + crisp answers

Q: Why Builder Pattern? A: To construct complex objects step-by-step, avoid telescoping constructors, enforce validation, and keep objects immutable.

Q: Why not always use Builder in Kotlin? A: Kotlin has named params + default args + data class copy(), so builder is only needed when validation / multi-step construction / DSL readability is important.

Q: How do you make sure the built object is immutable? A: Private constructor + builder holds mutable state + build() returns object with val fields + defensive copies for collections (toMap()).

Q: Where do you put validation? A: In build(). Builders are like “form filling”; build() is “submit & validate”.

Mark this when you can explain the answer in your own words.

Share & Help Others

Help fellow developers prepare for interviews

Sharing helps the Android community grow 💚

Keep practising