# Builder

> Assemble a complex object through named, step-by-step calls and validate it once in build(), instead of a constructor with many parameters.

- kind: pattern
- category: creational
- canonical: https://aipatterns.dev/design-patterns/builder
- inspired-by: https://refactoring.guru/design-patterns/builder

## Intent

Separate the construction of a complex object from the object itself. A builder collects the parts through small, named calls, then checks that the combination is valid and returns a finished object, usually immutable, in a single `build()` step.

## Forces

- **Many optional parts** vs **readable call sites**: `new Email(a, b, null, null, c, [], true)` hides which argument is which.
- **Immutable result** vs **piecemeal assembly**: the parts arrive across loops and conditionals, but the finished object should never change.
- **Validation of the whole** vs **validation per field**: some rules span fields ("at least one recipient", "total attachment size").
- **One construction process** vs **several representations**: sometimes the same steps should produce different outputs.

## Therefore

**Give the object a companion builder with one method per part, and make `build()` the only way to get a finished, validated instance.**

The builder is mutable and short-lived. The product is complete and, ideally, immutable. Call sites read as a list of named steps, and invalid combinations fail in one place. When the same sequence of steps is reused, put it in a *director*: a function or class that drives the builder.

## Structure

```mermaid
classDiagram
  class EmailBuilder {
    -string[] to
    -string subject
    -Attachment[] attachments
    +addTo(address) EmailBuilder
    +subject(text) EmailBuilder
    +attach(file) EmailBuilder
    +build() EmailMessage
  }
  class EmailMessage {
    -string from
    -string[] to
    -string subject
    +totalBytes() int
  }
  class Attachment {
    -string filename
    -int bytes
  }
  class DigestJob {
    +compose(team, report) EmailMessage
  }
  DigestJob ..> EmailBuilder : drives
  EmailBuilder ..> EmailMessage : builds
  EmailMessage *-- Attachment
```

### Participants

- **Builder** (`EmailBuilder`): mutable; exposes one method per part and returns itself for chaining.
- **Product** (`EmailMessage`): the finished object. Has no setters and is only created by `build()`.
- **Director** (`DigestJob`, here a function `weeklyDigest`): optional; encodes a reusable sequence of builder calls.
- **Client**: creates a builder, calls steps (directly or through a director), and calls `build()`.

## Use it when

- A constructor has four or more parameters, several optional, and call sites pass `null` or `undefined` to skip them.
- Parts are collected incrementally, in loops or conditionals, before the object is usable.
- The finished object must be immutable, but constructing it in one expression is impractical.
- Validation spans several fields and must happen before the object exists.
- The same assembly steps must produce different representations (a MIME message and a plain-text preview, for example).

## Do not use it when

- The parameters are independent and all known at the call site. The language's native optional arguments cover it: named or keyword parameters with defaults in Python, Kotlin, C# and Swift (`EmailMessage(to=[...], subject="...")`), a typed options object in TypeScript (`new EmailMessage({ to, subject })`), a struct literal or functional options in Go (`New(WithCC(...))`). Reserve Builder for step-wise accumulation or validation that spans fields.
- The object has two or three required fields. A constructor is shorter and enforces them at compile time, which a builder does not.
- The builder would mirror every field one to one with no validation and no accumulation. That is a second class doing the constructor's job.

## Application grill

**Inspect first:** the constructor or parameter sprawl the pattern would fix, and every construction site that would have to migrate; whether a validation library (zod, class-validator, pydantic) already exists that build() should call.

**Missing pieces:** validation at the trust boundary: if the object's invariants are checked nowhere, put them in build(). A validation library is justified when the same rules are enforced in several places, not for one type.

**Ask before you build:**

- **Scale:** how many optional parameters are real today - an options object covers modest cases without builder ceremony?
- **Performance:** is the object built per element in a loop where extra allocations show?
- **Reliability:** which invariants must fail loudly in build(), and which may surface later?
- **Budget:** builder plus target roughly doubles the surface for one type; how many construction sites pay that back?
- **Implementation:** does build() return the plain type or a validated one, and can callers still construct the target directly?

## Implementation steps

1. List the product's parts and mark which are required, which are optional, and which rules span several fields.
2. Make the product's constructor private or package-private (where the language allows) and its fields read-only.
3. Write the builder with one method per part. Collection parts get `add`-style methods; scalar parts get setters. Return the builder for chaining.
4. Put all cross-field validation in `build()`, and copy collections into the product so later builder calls can't change it.
5. Replace call sites one at a time, running tests after each.
6. If several call sites repeat the same sequence of steps, extract it into a director function.

## Example

A reporting service sends weekly digests. Recipients come from a team list, the attachment is optional, and the mail provider rejects messages over 10 MB, so the size rule must be checked before sending.

```ts title="email-builder.ts"
interface Attachment {
  filename: string;
  bytes: number;
}

const MAX_BYTES = 10 * 1024 * 1024;

class EmailMessage {
  constructor(
    readonly from: string,
    readonly to: readonly string[],
    readonly subject: string,
    readonly body: string,
    readonly attachments: readonly Attachment[],
  ) {}

  totalBytes(): number {
    return this.attachments.reduce((sum, a) => sum + a.bytes, 0);
  }
}

class EmailBuilder {
  private readonly to: string[] = [];
  private readonly attachments: Attachment[] = [];
  private subjectText = "";
  private bodyText = "";

  constructor(private readonly from: string) {}

  addTo(address: string): this {
    this.to.push(address);
    return this;
  }
  subject(text: string): this {
    this.subjectText = text;
    return this;
  }
  body(text: string): this {
    this.bodyText = text;
    return this;
  }
  attach(file: Attachment): this {
    this.attachments.push(file);
    return this;
  }

  build(): EmailMessage {
    if (this.to.length === 0) throw new Error("at least one recipient is required");
    if (!this.subjectText.trim()) throw new Error("subject is required");
    const msg = new EmailMessage(this.from, [...this.to], this.subjectText, this.bodyText, [...this.attachments]);
    if (msg.totalBytes() > MAX_BYTES) throw new Error("attachments exceed 10 MB");
    return msg;
  }
}

// Director: one reusable sequence of steps, including loops and conditionals.
function weeklyDigest(team: string[], reportBytes: number | null): EmailMessage {
  const b = new EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached.");
  team.forEach((member) => b.addTo(member));
  if (reportBytes !== null) b.attach({ filename: "usage.csv", bytes: reportBytes });
  return b.build();
}

const msg = weeklyDigest(["ana@example.com", "li@example.com"], 48_000);
console.log(msg.to.length, msg.totalBytes()); // 2 48000
try {
  weeklyDigest([], null);
} catch (e) {
  console.log((e as Error).message); // at least one recipient is required
}
```

```python title="email_builder.py"
from dataclasses import dataclass, field

MAX_BYTES = 10 * 1024 * 1024

@dataclass(frozen=True)
class Attachment:
    filename: str
    bytes: int

@dataclass(frozen=True)
class EmailMessage:
    sender: str
    to: tuple[str, ...]
    subject: str
    body: str = ""
    attachments: tuple[Attachment, ...] = ()

    def total_bytes(self) -> int:
        return sum(a.bytes for a in self.attachments)

# Keyword arguments already name every field; the builder earns its place
# only by accumulating parts step by step and validating the whole.
@dataclass
class EmailBuilder:
    sender: str
    _to: list[str] = field(default_factory=list)
    _attachments: list[Attachment] = field(default_factory=list)
    _subject: str = ""
    _body: str = ""

    def add_to(self, address: str) -> "EmailBuilder":
        self._to.append(address)
        return self

    def subject(self, text: str) -> "EmailBuilder":
        self._subject = text
        return self

    def body(self, text: str) -> "EmailBuilder":
        self._body = text
        return self

    def attach(self, file: Attachment) -> "EmailBuilder":
        self._attachments.append(file)
        return self

    def build(self) -> EmailMessage:
        if not self._to:
            raise ValueError("at least one recipient is required")
        if not self._subject.strip():
            raise ValueError("subject is required")
        msg = EmailMessage(self.sender, tuple(self._to), self._subject, self._body, tuple(self._attachments))
        if msg.total_bytes() > MAX_BYTES:
            raise ValueError("attachments exceed 10 MB")
        return msg

def weekly_digest(team: list[str], report_bytes: int | None) -> EmailMessage:
    b = EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached.")
    for member in team:
        b.add_to(member)
    if report_bytes is not None:
        b.attach(Attachment("usage.csv", report_bytes))
    return b.build()

if __name__ == "__main__":
    msg = weekly_digest(["ana@example.com", "li@example.com"], 48_000)
    print(len(msg.to), msg.total_bytes())  # 2 48000
    try:
        weekly_digest([], None)
    except ValueError as e:
        print(e)  # at least one recipient is required
```

```go title="email_builder.go"
package main

import (
	"errors"
	"fmt"
)

const maxBytes = 10 * 1024 * 1024

type Attachment struct {
	Filename string
	Bytes    int
}

type EmailMessage struct {
	From, Subject, Body string
	To                  []string
	Attachments         []Attachment
}

func (m EmailMessage) TotalBytes() int {
	total := 0
	for _, a := range m.Attachments {
		total += a.Bytes
	}
	return total
}

type EmailBuilder struct{ msg EmailMessage }

func NewEmailBuilder(from string) *EmailBuilder {
	return &EmailBuilder{msg: EmailMessage{From: from}}
}

func (b *EmailBuilder) AddTo(addr string) *EmailBuilder {
	b.msg.To = append(b.msg.To, addr)
	return b
}

func (b *EmailBuilder) Subject(s string) *EmailBuilder {
	b.msg.Subject = s
	return b
}

func (b *EmailBuilder) Body(s string) *EmailBuilder {
	b.msg.Body = s
	return b
}

func (b *EmailBuilder) Attach(a Attachment) *EmailBuilder {
	b.msg.Attachments = append(b.msg.Attachments, a)
	return b
}

// Build validates the whole message and returns a copy the builder can't mutate.
func (b *EmailBuilder) Build() (EmailMessage, error) {
	switch {
	case len(b.msg.To) == 0:
		return EmailMessage{}, errors.New("at least one recipient is required")
	case b.msg.Subject == "":
		return EmailMessage{}, errors.New("subject is required")
	case b.msg.TotalBytes() > maxBytes:
		return EmailMessage{}, errors.New("attachments exceed 10 MB")
	}
	m := b.msg
	m.To = append([]string(nil), b.msg.To...)
	m.Attachments = append([]Attachment(nil), b.msg.Attachments...)
	return m, nil
}

func weeklyDigest(team []string, reportBytes int) (EmailMessage, error) {
	b := NewEmailBuilder("reports@example.com").Subject("Weekly usage digest").Body("Numbers attached.")
	for _, member := range team {
		b.AddTo(member)
	}
	if reportBytes > 0 {
		b.Attach(Attachment{Filename: "usage.csv", Bytes: reportBytes})
	}
	return b.Build()
}

func main() {
	msg, _ := weeklyDigest([]string{"ana@example.com", "li@example.com"}, 48_000)
	fmt.Println(len(msg.To), msg.TotalBytes()) // 2 48000
	_, err := weeklyDigest(nil, 0)
	fmt.Println(err) // at least one recipient is required
}
```

```java title="Email.java"
import java.util.ArrayList;
import java.util.List;

public class Email {
    static final long MAX_BYTES = 10L * 1024 * 1024;

    record Attachment(String filename, long bytes) {}

    record EmailMessage(String from, List<String> to, String subject, String body, List<Attachment> attachments) {
        long totalBytes() { return attachments.stream().mapToLong(Attachment::bytes).sum(); }
    }

    static final class EmailBuilder {
        private final String from;
        private final List<String> to = new ArrayList<>();
        private final List<Attachment> attachments = new ArrayList<>();
        private String subject = "";
        private String body = "";

        EmailBuilder(String from) { this.from = from; }

        EmailBuilder addTo(String address) { to.add(address); return this; }
        EmailBuilder subject(String text) { subject = text; return this; }
        EmailBuilder body(String text) { body = text; return this; }
        EmailBuilder attach(Attachment file) { attachments.add(file); return this; }

        EmailMessage build() {
            if (to.isEmpty()) throw new IllegalStateException("at least one recipient is required");
            if (subject.isBlank()) throw new IllegalStateException("subject is required");
            // List.copyOf makes the product immune to later builder calls.
            var msg = new EmailMessage(from, List.copyOf(to), subject, body, List.copyOf(attachments));
            if (msg.totalBytes() > MAX_BYTES) throw new IllegalStateException("attachments exceed 10 MB");
            return msg;
        }
    }

    static EmailMessage weeklyDigest(List<String> team, Long reportBytes) {
        var b = new EmailBuilder("reports@example.com").subject("Weekly usage digest").body("Numbers attached.");
        team.forEach(b::addTo);
        if (reportBytes != null) b.attach(new Attachment("usage.csv", reportBytes));
        return b.build();
    }

    public static void main(String[] args) {
        var msg = weeklyDigest(List.of("ana@example.com", "li@example.com"), 48_000L);
        System.out.println(msg.to().size() + " " + msg.totalBytes()); // 2 48000
        try {
            weeklyDigest(List.of(), null);
        } catch (IllegalStateException e) {
            System.out.println(e.getMessage()); // at least one recipient is required
        }
    }
}
```

## Trade-offs

**Gains**
- Call sites name each part, so there are no positional `null` arguments.
- The product can be immutable even though it is assembled piece by piece.
- Cross-field validation runs once, before an invalid object can exist.
- A director captures a common recipe so call sites don't repeat it.

**Costs**
- A second class that must stay in sync with every field of the product.
- Required fields are checked at runtime in `build()`, not by the compiler. A constructor catches a missing argument earlier.
- Duplicates what named arguments, options objects or functional options already give you in many languages.

## Relations

- [Abstract Factory](https://aipatterns.dev/design-patterns/abstract-factory.md) returns finished, related objects in one call. Builder assembles one object over several calls.
- [Factory Method](https://aipatterns.dev/design-patterns/factory-method.md) decides which class to create. Builder decides how one instance is put together. A factory method can return a preconfigured builder.
- [Composite](https://aipatterns.dev/design-patterns/composite.md) trees are a common product of builders, since nested structures are tedious to construct by hand.
- [Prototype](https://aipatterns.dev/design-patterns/prototype.md) starts from a copy of an existing object. Builder starts from nothing. You can seed a builder from a prototype ("same as this message, but to a different team").

## Agent checklist

- [ ] The product has many optional parts, step-wise accumulation, or cross-field rules. Otherwise use a constructor, options object or keyword arguments.
- [ ] Required parts that the compiler could enforce are constructor parameters of the builder, not optional setters.
- [ ] `build()` validates the whole object and copies collections, so the product never shares mutable state with the builder.
- [ ] The product has no public setters and no way to be created except through `build()`, where the language allows it.
- [ ] Every rewritten call site produces an object equal to the one it produced before. Run the existing tests.
