Design Patterns, Explained Simply · Creational Patterns: Creating Objects Flexibly
Builder
Assemble a complex object step by step with readable, named steps and one final validation, instead of a constructor with ten positional arguments.
The problem
An HTTP client's request class has a constructor like Request("GET", url, None, None, 30, True, 3, {"Accept": "json"}, None). Nobody remembers which None is the body and which is the auth token. Some combinations are invalid, such as a GET with a body, but the constructor can't stop them, and the error appears far away when the request is sent.
Adding one more option means another parameter, or a new overloaded constructor ("telescoping constructors").
Real-world analogy
Ordering a sandwich at a counter: you choose the bread, then the filling, then the extras, step by step, and the person at the counter checks it makes sense ("we can't toast a salad"). At the end you get one finished sandwich. You never have to state all 12 choices in a fixed order in one breath.
Without the pattern
# What is True? What is 3? Which None is the body?
req = Request("POST", "https://api.example.com/users", None, '{"name": "Ada"}', 30, True, 3, {}, None)
The pattern
- Builder: an object with one method per step (
header(),timeout(),body()). Each method returns the builder itself so calls can be chained. build(): checks the combination is valid and returns the finished, ideally immutable, product.- Product: the complex object being built (
Request). - Director (optional): code that knows a standard recipe, such as
json_post(builder, url, data).
The code
class Request:
def __init__(self, method, url, headers, body, timeout):
self.method, self.url, self.headers = method, url, headers
self.body, self.timeout = body, timeout
def describe(self):
return (f"{self.method} {self.url} headers={len(self.headers)} "
f"body={'yes' if self.body else 'no'} timeout={self.timeout}s")
class RequestBuilder:
def __init__(self, url):
self._url, self._method = url, "GET"
self._headers, self._body, self._timeout = {}, None, 30
def method(self, method):
self._method = method
return self # returning self allows chaining
def header(self, name, value):
self._headers[name] = value
return self
def json(self, text):
self._body = text
return self.header("Content-Type", "application/json")
def timeout(self, seconds):
self._timeout = seconds
return self
def build(self): # one place to validate the whole combination
if self._method == "GET" and self._body is not None:
raise ValueError("a GET request cannot have a body")
return Request(self._method, self._url, dict(self._headers), self._body, self._timeout)
simple = RequestBuilder("https://api.example.com/users").build()
print(simple.describe())
create = (RequestBuilder("https://api.example.com/users")
.method("POST").header("Accept", "application/json")
.json('{"name": "Ada"}').timeout(5).build())
print(create.describe())
try:
RequestBuilder("https://api.example.com/users").json("{}").build()
except ValueError as e:
print("rejected:", e)
Output
GET https://api.example.com/users headers=0 body=no timeout=30s POST https://api.example.com/users headers=2 body=yes timeout=5s rejected: a GET request cannot have a body
class HttpRequest {
constructor(
readonly method: string,
readonly url: string,
readonly headers: Readonly<Record<string, string>>,
readonly body: string | null,
readonly timeout: number,
) {}
describe(): string {
return `${this.method} ${this.url} headers=${Object.keys(this.headers).length} ` +
`body=${this.body ? "yes" : "no"} timeout=${this.timeout}s`;
}
}
class RequestBuilder {
private _method = "GET";
private _headers: Record<string, string> = {};
private _body: string | null = null;
private _timeout = 30;
constructor(private url: string) {}
method(method: string): this { this._method = method; return this; } // returning this allows chaining
header(name: string, value: string): this { this._headers[name] = value; return this; }
json(text: string): this { this._body = text; return this.header("Content-Type", "application/json"); }
timeout(seconds: number): this { this._timeout = seconds; return this; }
build(): HttpRequest { // one place to validate the whole combination
if (this._method === "GET" && this._body !== null) throw new Error("a GET request cannot have a body");
return new HttpRequest(this._method, this.url, { ...this._headers }, this._body, this._timeout);
}
}
const simple = new RequestBuilder("https://api.example.com/users").build();
console.log(simple.describe());
const create = new RequestBuilder("https://api.example.com/users")
.method("POST").header("Accept", "application/json")
.json('{"name": "Ada"}').timeout(5).build();
console.log(create.describe());
try {
new RequestBuilder("https://api.example.com/users").json("{}").build();
} catch (e) {
console.log("rejected:", (e as Error).message);
}
Output
GET https://api.example.com/users headers=0 body=no timeout=30s POST https://api.example.com/users headers=2 body=yes timeout=5s rejected: a GET request cannot have a body
import java.util.LinkedHashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
HttpRequest simple = new HttpRequest.Builder("https://api.example.com/users").build();
System.out.println(simple.describe());
HttpRequest create = new HttpRequest.Builder("https://api.example.com/users")
.method("POST").header("Accept", "application/json")
.json("{\"name\": \"Ada\"}").timeout(5).build();
System.out.println(create.describe());
try {
new HttpRequest.Builder("https://api.example.com/users").json("{}").build();
} catch (IllegalStateException e) {
System.out.println("rejected: " + e.getMessage());
}
}
}
final class HttpRequest {
private final String method, url, body;
private final Map<String, String> headers;
private final int timeout;
private HttpRequest(Builder b) { // only the builder can create one
method = b.method; url = b.url; body = b.body; timeout = b.timeout;
headers = Map.copyOf(b.headers);
}
String describe() {
return method + " " + url + " headers=" + headers.size() + " body=" + (body != null ? "yes" : "no")
+ " timeout=" + timeout + "s";
}
static final class Builder {
private final String url;
private String method = "GET", body = null;
private final Map<String, String> headers = new LinkedHashMap<>();
private int timeout = 30;
Builder(String url) { this.url = url; }
Builder method(String m) { method = m; return this; } // returning this allows chaining
Builder header(String name, String value) { headers.put(name, value); return this; }
Builder json(String text) { body = text; return header("Content-Type", "application/json"); }
Builder timeout(int seconds) { timeout = seconds; return this; }
HttpRequest build() { // one place to validate the whole combination
if (method.equals("GET") && body != null) throw new IllegalStateException("a GET request cannot have a body");
return new HttpRequest(this);
}
}
}
Output
GET https://api.example.com/users headers=0 body=no timeout=30s POST https://api.example.com/users headers=2 body=yes timeout=5s rejected: a GET request cannot have a body
Run this on your computer — see “Get Started” for setup.
<?php
final class HttpRequest {
public function __construct(
public readonly string $method,
public readonly string $url,
public readonly array $headers,
public readonly ?string $body,
public readonly int $timeout,
) {}
public function describe(): string {
return sprintf("%s %s headers=%d body=%s timeout=%ds", $this->method, $this->url,
count($this->headers), $this->body !== null ? "yes" : "no", $this->timeout);
}
}
class RequestBuilder {
private string $method = "GET";
private array $headers = [];
private ?string $body = null;
private int $timeout = 30;
public function __construct(private string $url) {}
public function method(string $m): static { $this->method = $m; return $this; } // returning $this allows chaining
public function header(string $name, string $value): static { $this->headers[$name] = $value; return $this; }
public function json(string $text): static { $this->body = $text; return $this->header("Content-Type", "application/json"); }
public function timeout(int $seconds): static { $this->timeout = $seconds; return $this; }
public function build(): HttpRequest { // one place to validate the whole combination
if ($this->method === "GET" && $this->body !== null) throw new LogicException("a GET request cannot have a body");
return new HttpRequest($this->method, $this->url, $this->headers, $this->body, $this->timeout);
}
}
$simple = (new RequestBuilder("https://api.example.com/users"))->build();
echo $simple->describe(), "\n";
$create = (new RequestBuilder("https://api.example.com/users"))
->method("POST")->header("Accept", "application/json")
->json('{"name": "Ada"}')->timeout(5)->build();
echo $create->describe(), "\n";
try {
(new RequestBuilder("https://api.example.com/users"))->json("{}")->build();
} catch (LogicException $e) {
echo "rejected: ", $e->getMessage(), "\n";
}
Output
GET https://api.example.com/users headers=0 body=no timeout=30s POST https://api.example.com/users headers=2 body=yes timeout=5s rejected: a GET request cannot have a body
Every call reads like a sentence, defaults are filled in automatically, and an invalid combination fails at build(), right where the request is assembled.
When to use it
- An object has many optional parts or defaults, and positional constructors are unreadable.
- The finished object should be immutable, but you want to assemble it gradually.
- Some combinations are invalid and you want one place to check them.
- You build the same kind of object in several standard ways (a director or helper per recipe).
When not to use it
- The object has two or three required fields. A constructor is clearer.
- Python, TypeScript and PHP 8 have named arguments, default values and object literals (
Request(method="POST", timeout=5)), which remove most of the need for a Builder. Use one there mainly for validation or step-by-step assembly. - In Java, a
recordwith a compact constructor covers small immutable objects.
You've already used it
- Java:
StringBuilder,HttpRequest.newBuilder()injava.net.http, and Lombok's@Builder. - Query builders: Laravel's
DB::table('users')->where(...)->orderBy(...)->get(), SQLAlchemy'sselect(...).where(...), Knex in Node.js. - Test data builders:
aUser().withEmail("x").build()in test suites.
Key points
- Builder assembles a complex object step by step with named, chainable methods.
build()validates the combination and returns a finished (often immutable) object.- It replaces telescoping constructors and unreadable positional arguments.
- In languages with named arguments, prefer them unless you need validation or gradual assembly.
- Further reading: Refactoring Guru: Builder.
Practice questions
What problem does the Builder pattern solve?
Answer
Creating objects with many optional parameters or invalid combinations. Instead of long positional constructors (telescoping constructors), a builder offers named, chainable steps and a final build() that validates and returns the finished object.
Why do builder methods usually return the builder itself?
Answer
So calls can be chained (.method("POST").timeout(5).build()), which reads like a sentence and keeps the assembly in one expression.
Do you need the Builder pattern in Python or TypeScript?
Answer
Less often. Named/keyword arguments with defaults (Python, PHP 8) and object literals (TypeScript) already make construction readable. A builder is still useful when you need step-by-step assembly, validation of combinations, or a fluent API like a query builder.
Give a widely used example of the Builder pattern.
Answer
SQL query builders such as Laravel's query builder or SQLAlchemy's select(), Java's StringBuilder and java.net.http.HttpRequest.newBuilder().
Open in the app to mark lessons complete, take notes and track your progress.