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

A request builder with chaining and validationTry it yourself
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

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 record with a compact constructor covers small immutable objects.

You've already used it

  • Java: StringBuilder, HttpRequest.newBuilder() in java.net.http, and Lombok's @Builder.
  • Query builders: Laravel's DB::table('users')->where(...)->orderBy(...)->get(), SQLAlchemy's select(...).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.