Explore when Builder still helps in Scala and when case classes, defaults, and copy-based updates make a mutable builder unnecessary.
Builder in Scala: A staged or fluent construction approach used when object assembly is complex enough that plain constructors, defaults, and value copying are not expressive enough on their own.
Builder is one of the patterns most transformed by Scala. In many codebases, a classic mutable builder is unnecessary because case classes, named parameters, defaults, and copy already provide readable object construction.
Scala already gives you several alternatives:
copy for derived variantsIf those tools keep construction readable and valid, a builder may only add ceremony.
1final case class ReportConfig(
2 title: String,
3 includeCharts: Boolean = true,
4 theme: Theme = Theme.Light
5)
That is already quite expressive.
Builder becomes more useful when:
In those cases, a builder can separate assembly from the final validated value.
A Scala-friendly builder is often a small immutable state machine rather than a mutable bag of setters:
1final case class EmailDraft(to: Option[String] = None, subject: Option[String] = None, body: String = "") {
2 def withTo(value: String): EmailDraft = copy(to = Some(value))
3 def withSubject(value: String): EmailDraft = copy(subject = Some(value))
4 def withBody(value: String): EmailDraft = copy(body = value)
5
6 def build: Either[String, Email] =
7 for
8 t <- to.toRight("missing recipient")
9 s <- subject.toRight("missing subject")
10 yield Email(t, s, body)
11}
This keeps intermediate assembly explicit without hiding mutation in a supposedly functional codebase.
A builder is especially reasonable when:
In the stable domain core, a smaller validated type is usually preferable to carrying builder semantics deeper than necessary.
The design inherits patterns from other languages instead of using Scala’s stronger constructor tools first.
If the builder can still produce invalid results freely, it is not solving much.
The assembled mutable or half-complete shape starts leaking into code that should rely on the final validated value instead.
Use Builder only when creation is genuinely staged or complex. Prefer named arguments, defaults, companions, and immutable copy flows first. When a builder is necessary, keep it boundary-local and make the transition from partial assembly to valid value explicit.