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:
A Builder separates how you build an object from the object itself.
Use it when:
Classic Java-style builders were created to avoid:
new User("Akshay", null, null, null, 0, false, null, ...)
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:
isPro == true then subscriptionId must exist)NetworkRequestYou want:
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
)
}
}
}
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 chainbuild() is where validation livesheaders prevents accidental mutation after buildKotlin 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()
}
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.”
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.”
Request.Builder(), OkHttpClient.Builder()OneTimeWorkRequestBuilder, constraints builder-ish patternsThese are builders because configuration has many options + readable setup.
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.
Help fellow developers prepare for interviews
Sharing helps the Android community grow 💚