StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsBuilder

Builder

A creational pattern that assembles a complex object through named, chainable steps, so the finished thing only comes into existence once every choice is in.

Definition

Somewhere in production right now, a request is retrying 30 times with a 3 second timeout because someone transposed two integers in a constructor call, and the compiler agreed to all of it. Constructors take their arguments by position, and positions carry no meaning a reviewer can check. Builder is the Gang of Four creational pattern that assembles a complex object through named, chainable steps instead of one overloaded constructor call. The official intent reads: separate the construction of a complex object from its representation so that the same construction process can create different representations.


A builder is really just an object that collects settings one named call at a time, then assembles the real thing when you call build(). Each setter records a choice and hands the builder back, so calls chain into something a reviewer can actually read: .timeout(3) means the timeout is 3, and nobody has to count commas to learn it.

The Transposed Arguments

Here's the failure in its smallest form. A Request takes four positional arguments, and the two integers at the end, timeout and retries, are mutually transposable with no type error. Java codebases traditionally escape through a telescoping ladder of constructor overloads, one per argument count, which trades the soup for a different mess. Read the second call site and decide what it does before checking the comment:

The health check now retries 30 times with a 3 second timeout, almost certainly the reverse of what its author meant. Nothing flags it: both arguments are integers, both orders are plausible, and the signature reads identically either way. The reviewer would need the parameter list memorized to catch it, and parameter lists are exactly what nobody memorizes.

One Named Call at a Time

The builder version makes every choice carry its name. RequestBuilder takes only the required url up front, presets everything else to a default, and each setter returns the builder so the calls chain. The example at the bottom configures a health check with .timeout(3) and touches nothing else. Before reading past the code, predict the full output of describe(), including the two fields the chain never mentions:

The answer is GET api/health timeout=3s retries=0. The method and retry count came from the defaults baked into the builder's constructor, the timeout came from the one named call, and the transposition bug from the previous section is now unwritable: there is no second integer sitting next to 3 to confuse it with. Notice also what the chain returns before build(): a builder, not a request. The Request object never exists in a half-configured state, because nothing constructs it until every choice is in.

Who Does What

The Gang of Four version names four roles: the Product (Request), the Builder interface declaring the construction steps, the ConcreteBuilder that implements them and accumulates state (RequestBuilder), and a Director, a separate class that calls the steps in a fixed sequence to reproduce a known configuration. The Director is the role modern code quietly dropped. What developers call a builder today is the Director-less form, a fluent ConcreteBuilder driven directly by the client, and that's the version worth knowing cold. The Director earns its place back when the same multi-step recipe gets rebuilt in several parts of a codebase, where it becomes a named home for the recipe.


The whole arrangement works like ordering a custom sandwich: you call out choices one at a time, each addition lands on the same sandwich in progress, and you walk away with one finished thing. The analogy has a limit worth stating, and it's the safety claim. A half-made sandwich sits on the counter where anyone could grab it, but a half-made Request is unreachable, because until build() runs there is no request at all, only a builder holding notes about one.

Builder: Step-by-Step

Installing the pattern is four moves:

1. Freeze the Product

Request keeps one constructor that takes everything and a body that only assigns. All the flexibility is about to move into the builder, which frees the product to be immutable: once built, nothing edits it.

2. Move the Defaults Into the Builder

The builder's constructor takes only what's required (the url) and presets the rest: GET, 30 seconds, 0 retries. An empty chain now produces a valid request, and the defaults live in exactly one place.

3. Return Yourself From Every Setter

Each setter records one choice and returns the builder, which is the whole trick behind .method("POST").timeout(5) reading like a sentence. One setter that forgets to return self breaks every chain that touches it.

4. Assemble Only at build()

build() is the single point where all the choices meet, so it's where the real Request gets constructed and where rules spanning several fields, like "retries need a timeout", can finally be checked.
The Go and Rust Versions

Go replaced the builder class with something lighter: functional options. The idea comes from Rob Pike's post on self-referential functions and was popularized by Dave Cheney's functional options for friendly APIs. An option is just a function that edits the object under construction, so NewRequest(url, WithTimeout(3)) passes configuration as values. The Go tab in the code block above uses exactly this shape, and it solves a problem builders share with config structs: a zero in a struct field can't say whether it was chosen or just never set, while an absent option is unambiguous.


Rust kept the builder but gave it ownership semantics. The standard library's std::process::Command is the canonical example, chaining .arg() and .env() calls into a spawn. The Rust tab follows its consuming-self convention: each setter takes mut self by value and hands it back, and build() consumes the builder entirely. After building, the builder is gone, enforced by the compiler, which closes a loophole every other language leaves open: nobody can keep a stale builder around and build() twice expecting independent results.

The Named-Arguments Question

You might think Builder is a universal upgrade over constructors. In half the languages on this page it's closer to a workaround. Python and PHP accept named arguments directly, so Request(url, timeout=3) delivers the readability half of the pattern natively, with defaults handled in the signature. What named arguments don't deliver is the other half: a place for validation that sees every field at once, assembly that accumulates across multiple statements or functions before committing, and a guarantee that no half-configured object escapes. When none of those are in play, the builder is boilerplate, which is why Java needed Lombok's @Builder annotation to generate the noise automatically.


The other boundary worth drawing is against Abstract Factory. Both are creational and both hide concrete construction, but a factory's methods each return a finished product immediately, varying which family it belongs to. A builder returns nothing useful until the end, varying how one object gets assembled. Factory answers "which one?", builder answers "configured how?".

Best Practices

The chain is easy to write and easy to write badly, and these five details separate the two:


A setter sees one field, but build() sees the whole combination, and rules like "retries require a timeout" only exist at the combination level. Throwing from build() also guarantees an invalid product never exists at all.

Check Yourself

Two questions before you go. First: between RequestBuilder("api/health") and the .build() call, what exists in memory, and why does that answer carry the pattern's safety guarantee? Second: your language has named and default arguments, so the readability problem is already solved. Name two construction problems that still justify a builder. Both answers are on this page, the second one in The Named-Arguments Question.


Then go spot it in the wild, starting with the most-typed builder in existence: Java's StringBuilder, where every append() returns the builder to keep the chain alive. Protocol Buffers generates a builder for every message type because its message objects are immutable, which is this page's whole argument shipped as infrastructure at Google scale. Last, grep your own codebase for a constructor call with two adjacent arguments of the same type. Whoever wrote it got the order right from memory, and whoever edits it next gets to gamble.

the_transposed_arguments.py

class Request:
    def __init__(self, url: str, method: str, timeout: int, retries: int):
        self.url = url
        self.method = method
        self.timeout = timeout
        self.retries = retries

    def describe(self) -> str:
        return f"{self.method} {self.url} timeout={self.timeout}s retries={self.retries}"

# Reads fine when you wrote it five minutes ago
request = Request("api/users", "POST", 5, 0)
print(request.describe())  # POST api/users timeout=5s retries=0

# Is that timeout=3 retries=30, or did someone transpose them?
health = Request("api/health", "GET", 3, 30)
print(health.describe())  # GET api/health timeout=3s retries=30

builder.py

class Request:  # Product: assembled once, then never edited
    def __init__(self, url: str, method: str, timeout: int, retries: int):
        self.url = url
        self.method = method
        self.timeout = timeout
        self.retries = retries

    def describe(self) -> str:
        return f"{self.method} {self.url} timeout={self.timeout}s retries={self.retries}"

class RequestBuilder:  # Builder: collects choices one named call at a time
    def __init__(self, url: str):
        # Defaults live here, in exactly one place
        self._url = url
        self._method = "GET"
        self._timeout = 30
        self._retries = 0

    def method(self, method: str) -> "RequestBuilder":
        self._method = method
        return self  # returning self is what makes the calls chain

    def timeout(self, seconds: int) -> "RequestBuilder":
        self._timeout = seconds
        return self

    def retries(self, count: int) -> "RequestBuilder":
        self._retries = count
        return self

    def build(self) -> Request:
        # The Request doesn't exist until every choice is in
        return Request(self._url, self._method, self._timeout, self._retries)

# Example usage (Python's keyword arguments solve most of this natively;
# the builder shape matters most in languages without them)
request = RequestBuilder("api/health").timeout(3).build()
print(request.describe())  # GET api/health timeout=3s retries=0