Builder
The Builder pattern separates the construction of a complex object from the object itself.
Callers invoke setter-style methods on a builder — one for each field they need — and call
build() to obtain the finished object.
The pattern addresses a structural problem with constructors that have many parameters. A call
such as new EmailMessage(to, from, subject, body, cc, bcc, replyTo, htmlBody, attachments, priority)
requires the reader to count positions or scroll to the constructor definition to identify each
argument. Optional parameters must still be passed as null or an empty list, and adding a new
parameter forces every existing call site to be updated.
The same issue arises with HTTP request objects. A request can carry a URL, a method, a body, query parameters, custom headers, a timeout, a retry policy, and authentication credentials. Not every request uses all of these, yet a plain constructor requires a positional slot for each.
How the Builder solves it
A builder separates object construction from the object itself. You call setter-style methods on
the builder — one for each field you actually need — and call build() at the end to get the
finished object. Each setter returns self (or this), which lets you chain calls into a
readable sequence.
The resulting call chain reads like a sentence: you see exactly which fields were set, in plain terms, with no positional ambiguity.
Each setter returns the builder itself, so the chain can be written in one expression before
build() assembles the final immutable object.
Bad → Good
The telescoping constructor forces callers to pass every parameter in order, including the ones they do not need.
1# Bad: ten positional parameters, half of them optional nulls.
2# The caller must remember which position is which.
3class HttpRequest:
4 def __init__(
5 self,
6 url: str,
7 method: str,
8 headers: dict | None = None,
9 body: bytes | None = None,
10 timeout_ms: int = 5000,
11 retry_count: int = 0,
12 auth_token: str | None = None,
13 content_type: str | None = None,
14 ) -> None:
15 self.url = url
16 self.method = method
17 self.headers = headers or {}
18 self.body = body
19 self.timeout_ms = timeout_ms
20 self.retry_count = retry_count
21 self.auth_token = auth_token
22 self.content_type = content_type
23
24# At the call site, this is essentially unreadable:
25req = HttpRequest(
26 "https://api.payments.io/charge",
27 "POST",
28 None,
29 b'{"amount": 5000}',
30 10000,
31 3,
32 "Bearer tok_abc123",
33 "application/json",
34)
With a builder, only the fields you actually set appear in the call chain. The intent is clear
at a glance, and adding a new optional field to HttpRequest does not break any existing call
site.