On this page
Description
Builder is the creational pattern for objects with many optional parts: instead of a constructor with a long row of positional arguments, you set each part by name, in any order, and call build() at the end - which validates everything once and returns a finished, usually immutable object. This skill covers the classic fluent builder, what goes inside it, how Python's keyword arguments change the picture, and when a builder is overkill.
An HTTP request has a URL, a method, any number of headers, an optional body and a timeout. With constructors alone, calls look like this:
Java
new HttpRequest("https://api.example.com/users", "POST", headers, null, 5000, true, false);
Python
HttpRequest("https://api.example.com/users", "POST", headers, None, 5000, True, False)
Which argument is the timeout? What do true, false mean? Every optional part needs a placeholder, and each new option means another constructor overload - the telescoping constructor problem. Setters (new HttpRequest(), then setUrl(...)) avoid the positional mess, but they let the object be used while half-built and keep it mutable forever.
In Python, keyword arguments with defaults solve most of this: HttpRequest(url, method="POST", timeout_ms=5000) names every argument, and a frozen dataclass that validates in __post_init__ is immutable and valid without any builder. A builder still earns its place when an object is assembled step by step - from a config file, a parsed command, or a fluent query API.
We'll go from simple to more technical:
- The pattern: named, chainable steps and a final
build(). - Inside the builder: defaults, private constructors and validation across fields.
- Where you'll see builders in real libraries.
- When not to use one, and choosing among the creational patterns.
The pattern
Java
Pizza pizza = new PizzaBuilder()
.setSize("LARGE")
.addTopping("Cheese")
.addTopping("Olives")
.build();
Python
pizza = (PizzaBuilder()
.set_size("LARGE")
.add_topping("Cheese")
.add_topping("Olives")
.build())
Compare to new Pizza("LARGE", true, false, true, false, ...), where every caller has to remember what each positional boolean means, and every optional combination needs its own constructor overload.
Inside the builder
The builder is usually a static nested class. The product's constructor is private and takes the builder, so build() is the only way to make one - and build() validates first:
Java
final class HttpRequest {
private final String method, url, body;
private final int timeoutMs;
private HttpRequest(Builder b) { method = b.method; url = b.url; body = b.body; timeoutMs = b.timeoutMs; }
static class Builder {
private String method = "GET"; // defaults live in the builder
private String url, body;
private int timeoutMs = 30_000;
Builder method(String m) { this.method = m; return this; } // return this -> chaining
Builder url(String u) { this.url = u; return this; }
Builder body(String b) { this.body = b; return this; }
Builder timeoutMs(int t) { this.timeoutMs = t; return this; }
HttpRequest build() {
if (url == null) throw new IllegalArgumentException("url is required");
if (body != null && method.equals("GET")) // a rule across two fields
throw new IllegalArgumentException("GET request cannot have a body");
return new HttpRequest(this);
}
}
}
HttpRequest r = new HttpRequest.Builder().url("https://api.example.com/users").timeoutMs(5_000).build();
Python
from dataclasses import dataclass
from typing import Self
@dataclass(frozen=True)
class HttpRequest:
method: str
url: str
body: str | None
timeout_ms: int
class Builder:
def __init__(self):
self._method = "GET" # defaults live in the builder
self._url: str | None = None
self._body: str | None = None
self._timeout_ms = 30_000
def method(self, m: str) -> Self:
self._method = m
return self # return self -> chaining
def url(self, u: str) -> Self:
self._url = u
return self
def body(self, b: str) -> Self:
self._body = b
return self
def timeout_ms(self, t: int) -> Self:
self._timeout_ms = t
return self
def build(self) -> "HttpRequest":
if self._url is None:
raise ValueError("url is required")
if self._body is not None and self._method == "GET": # a rule across two fields
raise ValueError("GET request cannot have a body")
return HttpRequest(self._method, self._url, self._body, self._timeout_ms)
r = HttpRequest.Builder().url("https://api.example.com/users").timeout_ms(5_000).build()
A rule that involves several fields - "no body on a GET" - can only be checked in build(), once every field is known, because the steps can be called in any order. Validating there means the finished object is valid from the moment it exists, and its final fields keep it that way. (In Python, frozen=True does that job - but nothing stops a caller from calling HttpRequest(...) directly and skipping build(), so rules that must always hold belong in __post_init__.)
Where you'll see it
The JDK's own HTTP client builds requests this way - HttpRequest.newBuilder(uri).header(...).timeout(...).build() - as do Locale.Builder and most third-party HTTP clients: a request has many optional fields (headers, timeout, body, ...), and a builder makes specifying just the ones you need readable. In Python the fluent style shows up in query builders - SQLAlchemy's select(User).where(...).order_by(...), Django's filter(...).order_by(...) - where each step returns a new object instead of changing one. StringBuilder is only a loose cousin: it accumulates parts step by step and produces a String at the end, but it has no named options and no validation step.
Choosing among the creational patterns
| If you need... | Reach for | Because |
|---|---|---|
| Exactly one shared instance, by design | Singleton | Two copies would be a bug (e.g. two connection pools fighting over the same database connections). |
| One place that decides which concrete class to build | Factory Method | Callers depend on an interface; adding a type changes the factory, not every caller. |
| A matched family of related objects that must vary together | Abstract Factory | Swapping the factory swaps the whole family, so mismatched pieces can't be combined. |
| An object with many optional parts, assembled step by step | Builder (this lesson) | Named, optional steps instead of long positional constructors; validate once in build(). |
| Many near-identical objects that are expensive to set up | Prototype | Copy a ready-made template instead of repeating costly construction. |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| A builder for one or two required fields | ceremony with no benefit | a constructor or a record / dataclass |
| Validating in each step method | cross-field rules can't be checked | validate once in build() |
| Passing the builder's own list to the product | building again changes the earlier product | copy collections in build() |
| Sharing one builder between threads | interleaved steps corrupt the product | one builder per construction |
| (Python) Writing a builder where keyword arguments would do | extra code for what HttpRequest(url, timeout_ms=5000) already gives | keyword arguments + __post_init__ validation |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. A User has an id and an email, both required, nothing optional. Builder?
Show answer
No - a constructor or a record is clearer. A Builder pays off when there are several optional parts.
2. Why validate in build() rather than in each step method?
Show answer
Steps can run in any order, so only build() sees the final value of every field. Rules across fields (no body on a GET) can only be checked there - once - before an immutable, valid object is created.
3. Two threads share one builder and both call addTopping(...). What's wrong?
Show answer
A builder is mutable, so sharing it races like any other mutable object. Use one builder per construction, confined to one thread.
Key takeaways
- Reach for it when an object has several optional parameters; one or two required fields just need a constructor.
- Each step is named and optional, so calls read like a sentence and avoid positional-argument mistakes.
- Validate everything once in
build(), then return an immutable object. - Builder methods return
this(selfin Python) so the steps can be chained fluently. - Treat a builder as single-use and thread-confined - never share one between threads.