Builder — a creational pattern that allows creating complex objects step by step. Unlike a constructor with a dozen parameters, Builder assembles an object through a chain of calls, each configuring one field. The pattern is especially useful for objects with many optional parameters: network client configuration, database settings, alert and navigation builders. Learn more at Refactoring Guru: Builder.
Key Takeaways
Builder — a creational GoF pattern that separates the construction of a complex object from its representation. The same construction process can create different representations. Builder is useful when an object has many optional parameters and a constructor with ten fields is unreadable and inflexible. The pattern also solves the Telescoping Constructor anti-pattern, where the number of constructor overloads grows exponentially.
Builder structure includes an inner static Builder class with fields mirroring the main class fields. Each set-method returns Builder (this) for fluent chaining. The final build() method creates the target object by passing field values to a private constructor. The main class has a private constructor that accepts a Builder. Client: Object.builder().setField1(val1).setField2(val2).build().
When to use Builder — objects with 5+ fields where only 2-3 are required. Configuration objects (RequestConfig, DatabaseConfig). Objects with complex validation logic during creation. Objects that must be immutable after creation. In Android, Builder is actively used in the SDK: AlertDialog.Builder, Retrofit.Builder, OkHttpClient.Builder, NotificationCompat.Builder.
Kotlin Builder has two approaches: classic Java-style Builder (via a nested class) and Kotlin-style DSL builder (via a lambda with receiver). Java-style Builder is preferable for Android compatibility and when used with Java code. DSL builder is the idiomatic Kotlin way: a function accepts a lambda inside which this is the Builder context where fields can be assigned directly.
// Classic Builder
data class HttpConfig private constructor(
val baseUrl: String,
val timeout: Long = 30_000,
val retries: Int = 3,
val headers: Map<String, String> = emptyMap()
) {
class Builder {
private var baseUrl: String = ""
private var timeout: Long = 30_000
private var retries: Int = 3
private var headers: MutableMap<String, String> = mutableMapOf()
fun baseUrl(url: String) = apply { this.baseUrl = url }
fun timeout(ms: Long) = apply { this.timeout = ms }
fun retries(n: Int) = apply { this.retries = n }
fun header(key: String, value: String) = apply { headers[key] = value }
fun build(): HttpConfig {
require(baseUrl.isNotBlank()) { "baseUrl is required" }
return HttpConfig(baseUrl, timeout, retries, headers)
}
}
}
// Usage
val config = HttpConfig.Builder()
.baseUrl("https://api.example.com")
.timeout(15_000)
.header("Authorization", "Bearer token")
.build()
Kotlin DSL builder — an alternative without a nested class. A builder function accepts a lambda in the context of a builder object. This is idiomatic for Kotlin and does not require write-fields. DSL builders are actively used in Ktor Client, kotlinx.serialization, Compose (Modifier). DSL builder is incompatible with Java and is not suitable for libraries with a Java API.
Swift Builder — Swift does not have a built-in Builder pattern, but a fluent interface is easily implemented through methods returning Self. Each method configures a property and returns self. Unlike Kotlin, Swift does not require a separate Builder class — you can return the object itself if it is mutable during assembly. For immutable objects, a nested Builder class is used similarly to Kotlin.
struct NetworkRequest {
let url: String
let method: HTTPMethod
let headers: [String: String]
let body: Data?
let timeout: TimeInterval
final class Builder {
private var url: String = ""
private var method: HTTPMethod = .get
private var headers: [String: String] = [:]
private var body: Data? = nil
private var timeout: TimeInterval = 30
func withURL(_: String) -> Self { /* self */ }
func withMethod(_: HTTPMethod) -> Self { /* self */ }
func withHeader(key: String, value: String) -> Self { /* self */ }
func withBody(_: Data) -> Self { /* self */ }
func withTimeout(_: TimeInterval) -> Self { /* self */ }
func build() throws -> NetworkRequest {
guard !url.isEmpty else { throw BuilderError.missingURL }
return NetworkRequest(
url: url, method: method, headers: headers,
body: body, timeout: timeout
)
}
}
}
Result Builders — Swift 5.4 introduced @resultBuilder — a language mechanism for declarative structure building. SwiftUI, AttributedString, SceneBuilder use result builders. This is an alternative to the classic Builder: instead of a chain of set-methods, result builder uses a code block with elements that the compiler assembles into an array or tree. @ViewBuilder in SwiftUI is the most famous example: inside body you can write if, switch, ForEach, and the compiler builds a View from conditions.
Telescoping Constructor — an anti-pattern where a class has many overloaded constructors with different sets of parameters. For example, three constructors: HttpConfig(url), HttpConfig(url, timeout), HttpConfig(url, timeout, retries). As parameters grow, the number of constructors grows exponentially — for n optional fields you need n! combinations. Builder solves this problem by allowing you to specify only the needed fields.
| Characteristic | Telescoping Constructor | Builder | Kotlin named args |
|---|---|---|---|
| Code volume | Exponential growth | Linear growth | Minimal |
| Readability | Low (which parameter is which?) | High (method + name) | High (name = value) |
| Immutability | Immutable | Immutable | Immutable |
| Java compatibility | Full | Full | None (Kotlin only) |
| Validation | In each constructor | In build() — once | In init() |
Kotlin named arguments + default values — an elegant alternative to Builder in pure Kotlin projects. Constructor parameters have default values, the client passes only what is needed: HttpConfig(baseUrl = url, timeout = 15_000). The drawback is the inability to validate required fields at compile time. Builder provides required fields through the Builder constructor (baseUrl is required). For Java libraries, Builder remains the de facto standard.
Builder in Android SDK — one of the most common patterns in the standard library. AlertDialog.Builder: new AlertDialog.Builder(context).setTitle().setMessage().setPositiveButton().create(). Retrofit.Builder: new Retrofit.Builder().baseUrl().addConverterFactory().build(). OkHttpClient.Builder: new OkHttpClient.Builder().connectTimeout().addInterceptor().build(). NotificationCompat.Builder: setContentTitle().setContentText().setSmallIcon().build().
Why Google uses Builder — backward compatibility. Adding a new method to Builder does not break existing code. If Google used a constructor with 20 parameters, each new field would require a new overload. Builder allows adding set-methods over years without breaking changes. For example, NotificationCompat.Builder added setBubbleMetadata() in Android 11 without affecting existing code.
Builder in Kotlin libraries — Ktor (HttpClientBuilder), Coil (ImageRequest.Builder), Room (Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build()), Navigation (NavOptionsBuilder). In Kotlin projects, Builder is often combined with DSL: Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build(). The pattern remains relevant for public APIs where backward compatibility and Java interop are important.
Frequently Asked Questions
Builder is overkill for objects with 1-3 fields — a regular constructor or data class is clearer. It is also overkill in Kotlin projects without Java interop, where named arguments + default values solve the same task more simply. Builder is justified for 5+ fields, complex validation, or Java APIs where named arguments are not available.
Builder creates one complex object step by step (field configuration), Factory creates an object entirely by type or parameters. Builder answers the question «how to assemble?», Factory answers «what to create?». Builder is often combined with Factory: Factory selects the type, Builder configures the fields.
In SwiftUI, the role of Builder is performed by result builders (@ViewBuilder, @SceneBuilder) and View modifiers (.font(), .padding()). Classic Builder is not needed because SwiftUI uses a declarative approach and fluent modifiers. For UIKit components, Builder is useful: UIAlertController, URLRequest, NSAttributedString.
Builder typically does not require thread safety since it is used in a single thread to assemble an object. If Builder is used in a multithreaded environment (a rare case), synchronize each set-method and build(). An alternative — Immutable Builder: each set-method returns a new Builder instance with the modified field.
Retrofit.Builder is a public library API that must work without a DI container. Builder provides configuration flexibility (baseUrl, converters, interceptors, custom call adapters) without dependencies on Dagger or other DI frameworks. Inside an application, DI can create Retrofit once through Builder, but Builder itself remains part of the Retrofit public API.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also