Question
I am new to Kotlin and exploring how to convert some existing Java libraries to Kotlin.
Those libraries contain many POJOs with getters, setters, and separate Builder classes. I want to understand the best way to implement the Builder design pattern in Kotlin for a simple object with a few parameters.
Here is my current attempt, converted from Java:
class Car private constructor(builder: Car.Builder) {
var model: String? = null
var year: Int = 0
init {
this.model = builder.model
this.year = builder.year
}
companion object Builder {
var model: String? = null
private set
var year: Int = 0
private set
fun model(model: String): Builder {
this.model = model
return this
}
fun year(year: Int): Builder {
this.year = year
return this
}
fun build(): Car {
val car = Car(this)
return car
}
}
}
How should a Builder be written correctly in Kotlin, and is this pattern still the best approach for simple classes?
Short Answer
By the end of this page, you will understand how the Builder pattern works in Kotlin, why a direct Java-style conversion is often not the best choice, and how Kotlin features such as primary constructors, named arguments, default parameter values, and data class often replace builders for simple objects. You will also see how to write a proper builder in Kotlin when you truly need one.
Concept
Kotlin can implement the Builder pattern, but in many cases you do not need it.
In Java, builders are commonly used because constructors can become hard to read when they have many parameters, especially optional ones. A builder solves this by letting you set values step by step before creating the object.
Kotlin already provides language features that solve much of this problem:
- Primary constructors let you declare properties directly in the class header.
- Named arguments make object creation easier to read.
- Default values make optional parameters simple.
data classgives you common object features automatically.copy()lets you create modified versions of immutable objects.
For a simple class like Car, Kotlin code is usually much cleaner without a builder.
For example, instead of this Java-style idea:
val car = Car.Builder()
.model("Civic")
.year(2024)
.build()
Kotlin often prefers:
val car = Car(model = "Civic", year = 2024)
That is shorter, clearer, and idiomatic.
However, builders are still useful when:
- an object has many optional fields
- object creation happens in stages
Mental Model
Think of a builder like a paper order form.
- The final object is the finished car order.
- The builder is the form you fill out before submitting.
- Each field such as
modeloryearis a box on the form. - Calling
build()is like submitting the form and receiving the final result.
In Kotlin, though, if the order is simple, you often do not need a separate form at all. You can just hand over the details directly:
Car(model = "Civic", year = 2024)
So the key question is:
- Is the object simple enough to create directly? Use a constructor.
- Is construction complex, staged, or validated? A builder may help.
Syntax and Examples
1. The Kotlin way for simple objects
For simple POJOs, prefer a data class with constructor parameters:
data class Car(
val model: String,
val year: Int = 0
)
Usage:
val car1 = Car(model = "Civic", year = 2024)
val car2 = Car(model = "Corolla")
Why this is good:
modelis requiredyearis optional because it has a default value- named arguments make calls readable
data classautomatically gives youtoString(),equals(),hashCode(), andcopy()
2. A proper Builder in Kotlin
If you really want the Builder pattern, create a separate nested Builder class:
Step by Step Execution
Consider this builder:
class Car private constructor(
val model: String,
val year: Int
) {
class Builder {
private var model: String? = null
private var year: Int = 0
fun model(model: String) = apply {
this.model = model
}
fun year(year: Int) = apply {
this.year = year
}
fun build(): Car {
require(!model.isNullOrBlank()) { "Model is required" }
return Car(model!!, year)
}
}
}
val car = Car.Builder()
.model("Civic")
.year(2024)
.build()
What happens step by step
1. Car.Builder()
Real World Use Cases
Builders are useful in several real programming situations.
Configuration objects
A library may need a lot of optional settings:
val config = HttpClientConfig.Builder()
.baseUrl("https://api.example.com")
.timeout(5000)
.retries(3)
.build()
Validation before construction
If some fields must be checked together, a builder can collect values and validate them at the end.
Example:
- username is required
- password must meet rules
- age must be positive
Step-by-step object assembly
Some objects are naturally built in phases, such as:
- SQL queries
- UI dialogs
- network requests
- report generation
Java interoperability
If your Kotlin library will be used from Java, a builder may still be a good API design because Java does not have named arguments.
Immutable objects with many options
Builders are often used when the final object should be immutable, but setting all values in one constructor would be inconvenient.
Real Codebase Usage
In real Kotlin codebases, developers often avoid builders for simple models and use more idiomatic patterns.
Common Kotlin patterns instead of builders
Primary constructor + default values
data class User(
val name: String,
val age: Int = 0,
val active: Boolean = true
)
Named arguments
val user = User(name = "Ava", active = false)
copy() for modification
val original = User(name = "Ava", age = 25)
val updated = original.copy(active = false)
When builders still appear in real projects
Guard clauses and validation
Builders often centralize validation:
fun build: User {
require(name.isNotBlank()) { }
require(age >= ) { }
User(name, age)
}
Common Mistakes
1. Using a companion object as the builder
This is the main problem in your attempt.
Broken idea:
companion object Builder {
var model: String? = null
var year: Int = 0
}
Why it is wrong:
- a
companion objectis shared by the class - all callers use the same state
- this can create bugs when multiple objects are built
Use a separate class Builder instead.
2. Translating Java too literally
Beginners often convert Java code line by line instead of using Kotlin features.
Less idiomatic Kotlin:
class Car private constructor(builder: Builder) {
var model: String? = null
var year: Int = 0
}
Better Kotlin for simple cases:
( model: String, year: = )
Comparisons
| Approach | Best for | Pros | Cons |
|---|---|---|---|
| Primary constructor | Simple objects | Short, idiomatic, easy to read | Less useful for staged creation |
| Constructor + default values | Objects with optional fields | Very concise, no extra builder class | Can become long with many parameters |
data class | Immutable value objects | Auto-generated useful methods | Not always enough for complex creation rules |
| Builder pattern | Complex construction or Java-friendly APIs | Fluent, can validate before creation | More code and more maintenance |
copy() on data class | Updating immutable objects | Very readable for small changes | Only works after an object already exists |
Cheat Sheet
Quick reference
Idiomatic Kotlin for simple objects
data class Car(
val model: String,
val year: Int = 0
)
Usage:
val car = Car(model = "Civic", year = 2024)
val car2 = Car(model = "Corolla")
Proper builder in Kotlin
class Car private constructor(
val model: String,
val year: Int
) {
class Builder {
private var model: String? = null
private var year: Int = 0
fun model(model: String) = apply { this.model = model }
= apply { .year = year }
: Car {
require(!model.isNullOrBlank()) { }
Car(model!!, year)
}
}
}
FAQ
Should I use the Builder pattern in Kotlin?
Usually not for simple classes. Kotlin's named arguments and default values often make builders unnecessary.
What is wrong with using a companion object as a builder?
A companion object is shared across all instances of the class, so the builder state becomes global and unsafe.
What should I use instead of a builder for a simple POJO in Kotlin?
Use a primary constructor, often with a data class, named arguments, and default values.
When is a builder still useful in Kotlin?
When construction is complex, requires validation, happens in stages, or when designing APIs for Java callers.
Why is apply commonly used in Kotlin builders?
Because it lets you modify the current builder and return it in one expression, which supports fluent chaining.
Should final properties be var or val?
Use val when the object should be immutable after creation. This is common in Kotlin.
Can a data class replace a builder?
Often yes, especially for simple immutable objects with optional fields and readable named arguments.
Mini Project
Description
Create a small Kotlin model for building a database connection configuration. This project demonstrates when a builder is useful: you have required values, optional values, and validation before creating the final object.
Goal
Build a DatabaseConfig object using a Kotlin builder with fluent methods and validation in build().
Requirements
- Create a
DatabaseConfigclass withhost,port,database, andsslEnabledproperties. - Make
hostanddatabaserequired before building. - Give
porta default value. - Use a nested
Builderclass with fluent setter methods. - Validate that
portis positive inbuild().
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.