Builder
Some objects have a handful of required fields and a long tail of optional ones — most of the time you only need a few of them set. The Builder pattern answers a practical question: how do you construct an object with many optional pieces, step by step, without a constructor call that's unreadable and dangerously easy to get wrong?
⚠️ The Problem
Imagine an HttpRequest class with a constructor that takes a method, a URL,
headers, a body, a timeout, and a retry count — six parameters, four of them optional. A call
site ends up looking like new HttpRequest("POST", url, null, body, 3000, null).
This is the classic telescoping constructor problem: nothing about that call
tells you which position is the timeout and which is the retry count, both integers, both easy
to swap by accident and have the code silently misbehave — a request that should time out in 3
seconds instead retries 3000 times, and the compiler never complains because the types line
up. As more optional fields get added, every constructor gets longer and every existing call
site becomes harder to read correctly.
✅ The Solution
Pull construction out into a separate Builder object with one chainable
method per optional piece — setMethod(), setHeader(),
setTimeout() — each returning the builder itself so calls can be chained. A final
build() method assembles everything that was configured into one immutable
finished object. Every call site now reads like a labeled sentence instead of a row of unlabeled
values, it's impossible to swap two parameters of the same type by accident, and the
construction logic is separated from what the final object's fields actually look like.
Example 1 — building an HTTP request
class HttpRequest {
constructor(method, url, headers, body, timeoutMs) {
this.method = method;
this.url = url;
this.headers = headers;
this.body = body;
this.timeoutMs = timeoutMs;
Object.freeze(this); // immutable once built
}
}
class HttpRequestBuilder {
#method = "GET";
#url;
#headers = {};
#body = null;
#timeoutMs = 5000;
setMethod(method) {
this.#method = method;
return this; // enables chaining
}
setUrl(url) {
this.#url = url;
return this;
}
setHeader(key, value) {
this.#headers[key] = value;
return this;
}
setBody(body) {
this.#body = body;
return this;
}
setTimeout(ms) {
this.#timeoutMs = ms;
return this;
}
build() {
if (!this.#url) throw new Error("url is required");
return new HttpRequest(this.#method, this.#url, this.#headers, this.#body, this.#timeoutMs);
}
}
const request = new HttpRequestBuilder()
.setMethod("POST")
.setUrl("https://api.example.com/orders")
.setHeader("Content-Type", "application/json")
.setBody(JSON.stringify({ item: "mug", qty: 2 }))
.setTimeout(3000)
.build();
console.log(request);
Compare this to a six-argument constructor call: every value here is labeled by the method
name that set it, unused optional fields (like body for a GET request) can simply be
skipped, and timeoutMs can never be confused with some other numeric field because
it's set through its own named method.
Example 2 — a secondary context: building a pizza order
public class PizzaOrder {
public string Size { get; }
public string Crust { get; }
public IReadOnlyList<string> Toppings { get; }
public bool ExtraCheese { get; }
public PizzaOrder(string size, string crust, List<string> toppings, bool extraCheese) {
Size = size;
Crust = crust;
Toppings = toppings.AsReadOnly();
ExtraCheese = extraCheese;
}
}
public class PizzaOrderBuilder {
private string _size = "Medium";
private string _crust = "Regular";
private List<string> _toppings = new List<string>();
private bool _extraCheese = false;
public PizzaOrderBuilder SetSize(string size) {
_size = size;
return this;
}
public PizzaOrderBuilder SetCrust(string crust) {
_crust = crust;
return this;
}
public PizzaOrderBuilder AddTopping(string topping) {
_toppings.Add(topping);
return this;
}
public PizzaOrderBuilder WithExtraCheese() {
_extraCheese = true;
return this;
}
public PizzaOrder Build() {
return new PizzaOrder(_size, _crust, _toppings, _extraCheese);
}
}
// Usage
var order = new PizzaOrderBuilder()
.SetSize("Large")
.SetCrust("Thin")
.AddTopping("Mushroom")
.AddTopping("Olives")
.WithExtraCheese()
.Build();
Same idea, a more everyday domain: toppings accumulate one at a time via
AddTopping() instead of requiring a pre-built list to be passed into a constructor,
and defaults like "Medium" size or "Regular" crust mean a caller who
only wants two toppings never has to think about the fields they don't care about.
👍 When To Use It
- A constructor would need many parameters, several of them optional, especially several of the same type where swapping two by position would compile fine but behave wrong.
- You want the constructed object to be immutable once finished, but still need a flexible, incremental way to assemble its state beforehand.
- The same construction process should be able to produce visibly different configurations (a plain request vs. one with custom headers and a body) without a family of overloaded constructors.
👎 When NOT To Use It
- When the object only has one or two fields, all required — a plain constructor or object literal is clearer and a builder is unnecessary machinery.
- When every field is always required and there's no meaningful "optional" subset — a builder's main benefit is handling optionality gracefully, which doesn't apply here.
- When the language already gives you named/default parameters that solve the same readability problem (e.g. Python keyword arguments) — a full Builder class may be redundant overhead in that case.
❓ FAQ
Isn't this the same as just using named/keyword arguments?
In languages with real keyword arguments (Python, Kotlin), a function call with named parameters solves the "which value is which" readability problem too, and is often simpler than a full Builder class. Builder earns its extra structure when construction needs validation across steps, incremental accumulation (like adding toppings one at a time), or when the target language (Java, C#, JavaScript) doesn't have first-class keyword arguments.
Does build() have to return an immutable object?
Not strictly, but it's the most common and most valuable form: once build()
runs, the resulting object's fields shouldn't be mutable out from under you, which is part
of what makes the object trustworthy after construction. If mutation after construction is
genuinely needed, that's usually a sign the object itself, not just its construction,
needs a different design.
What's a "director" I sometimes see mentioned alongside Builder?
The original GoF description includes an optional Director class that
knows a specific sequence of builder calls to produce a common configuration (e.g. a
StandardMargheritaDirector that always calls
SetCrust("Thin").AddTopping("Tomato").AddTopping("Basil")). It's useful when
the same "recipe" of builder calls is reused often, but plenty of real-world Builder usage
skips the Director and just chains builder calls directly, as both examples above do.
🙋 There Are No Dumb Questions
Q: Why not just use an object literal with optional keys instead of a whole Builder
class?
A: In a dynamically typed language, an object literal (or a constructor taking one options
object) often is a reasonable lightweight alternative, and many real codebases use it
instead of a formal Builder. Builder adds real value when you need method chaining with
validation at each step, incremental list-building (like adding toppings one at a time), or a
genuinely immutable result enforced by the type system — not just "fewer positional
arguments."
✅ Quick Check
What specific problem does Builder solve that a constructor with six parameters (four optional) does not?
Next up: what if creating a new object from scratch is expensive, but you need many objects that are almost identical to one you already have? That's the problem the Prototype pattern solves.