Creational Pattern Pattern Deep Dive

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

Primary Example — JavaScript
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

Secondary Example — C#
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.

← Back to
Next →