Design Patterns: Creational · Creational Patterns

Builder Pattern

Constructing a complex object step by step through chainable calls, instead of a constructor with a dozen parameters.

  • 6 min read
  • 12 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The pattern
  3. Inside the builder
  4. Where you'll see it
  5. Choosing among the creational patterns
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Builder is the creational pattern for objects with many optional parts: instead of a constructor with a long row of positional arguments, you set each part by name, in any order, and call build() at the end - which validates everything once and returns a finished, usually immutable object. This skill covers the classic fluent builder, what goes inside it, how Python's keyword arguments change the picture, and when a builder is overkill.

An HTTP request has a URL, a method, any number of headers, an optional body and a timeout. With constructors alone, calls look like this:

Java

new HttpRequest("https://api.example.com/users", "POST", headers, null, 5000, true, false);

Python

HttpRequest("https://api.example.com/users", "POST", headers, None, 5000, True, False)

Which argument is the timeout? What do true, false mean? Every optional part needs a placeholder, and each new option means another constructor overload - the telescoping constructor problem. Setters (new HttpRequest(), then setUrl(...)) avoid the positional mess, but they let the object be used while half-built and keep it mutable forever.

In Python, keyword arguments with defaults solve most of this: HttpRequest(url, method="POST", timeout_ms=5000) names every argument, and a frozen dataclass that validates in __post_init__ is immutable and valid without any builder. A builder still earns its place when an object is assembled step by step - from a config file, a parsed command, or a fluent query API.

We'll go from simple to more technical:

  1. The pattern: named, chainable steps and a final build().
  2. Inside the builder: defaults, private constructors and validation across fields.
  3. Where you'll see builders in real libraries.
  4. When not to use one, and choosing among the creational patterns.

The pattern

Java

Pizza pizza = new PizzaBuilder()
    .setSize("LARGE")
    .addTopping("Cheese")
    .addTopping("Olives")
    .build();

Python

pizza = (PizzaBuilder()
         .set_size("LARGE")
         .add_topping("Cheese")
         .add_topping("Olives")
         .build())

Compare to new Pizza("LARGE", true, false, true, false, ...), where every caller has to remember what each positional boolean means, and every optional combination needs its own constructor overload.

Inside the builder

The builder is usually a static nested class. The product's constructor is private and takes the builder, so build() is the only way to make one - and build() validates first:

Java

final class HttpRequest {
    private final String method, url, body;
    private final int timeoutMs;

    private HttpRequest(Builder b) { method = b.method; url = b.url; body = b.body; timeoutMs = b.timeoutMs; }

    static class Builder {
        private String method = "GET";              // defaults live in the builder
        private String url, body;
        private int timeoutMs = 30_000;

        Builder method(String m) { this.method = m; return this; }   // return this -> chaining
        Builder url(String u)    { this.url = u; return this; }
        Builder body(String b)   { this.body = b; return this; }
        Builder timeoutMs(int t) { this.timeoutMs = t; return this; }

        HttpRequest build() {
            if (url == null) throw new IllegalArgumentException("url is required");
            if (body != null && method.equals("GET"))                // a rule across two fields
                throw new IllegalArgumentException("GET request cannot have a body");
            return new HttpRequest(this);
        }
    }
}

HttpRequest r = new HttpRequest.Builder().url("https://api.example.com/users").timeoutMs(5_000).build();

Python

from dataclasses import dataclass
from typing import Self


@dataclass(frozen=True)
class HttpRequest:
    method: str
    url: str
    body: str | None
    timeout_ms: int

    class Builder:
        def __init__(self):
            self._method = "GET"                    # defaults live in the builder
            self._url: str | None = None
            self._body: str | None = None
            self._timeout_ms = 30_000

        def method(self, m: str) -> Self:
            self._method = m
            return self                             # return self -> chaining

        def url(self, u: str) -> Self:
            self._url = u
            return self

        def body(self, b: str) -> Self:
            self._body = b
            return self

        def timeout_ms(self, t: int) -> Self:
            self._timeout_ms = t
            return self

        def build(self) -> "HttpRequest":
            if self._url is None:
                raise ValueError("url is required")
            if self._body is not None and self._method == "GET":   # a rule across two fields
                raise ValueError("GET request cannot have a body")
            return HttpRequest(self._method, self._url, self._body, self._timeout_ms)


r = HttpRequest.Builder().url("https://api.example.com/users").timeout_ms(5_000).build()

A rule that involves several fields - "no body on a GET" - can only be checked in build(), once every field is known, because the steps can be called in any order. Validating there means the finished object is valid from the moment it exists, and its final fields keep it that way. (In Python, frozen=True does that job - but nothing stops a caller from calling HttpRequest(...) directly and skipping build(), so rules that must always hold belong in __post_init__.)

Where you'll see it

The JDK's own HTTP client builds requests this way - HttpRequest.newBuilder(uri).header(...).timeout(...).build() - as do Locale.Builder and most third-party HTTP clients: a request has many optional fields (headers, timeout, body, ...), and a builder makes specifying just the ones you need readable. In Python the fluent style shows up in query builders - SQLAlchemy's select(User).where(...).order_by(...), Django's filter(...).order_by(...) - where each step returns a new object instead of changing one. StringBuilder is only a loose cousin: it accumulates parts step by step and produces a String at the end, but it has no named options and no validation step.

Choosing among the creational patterns

If you need...Reach forBecause
Exactly one shared instance, by designSingletonTwo copies would be a bug (e.g. two connection pools fighting over the same database connections).
One place that decides which concrete class to buildFactory MethodCallers depend on an interface; adding a type changes the factory, not every caller.
A matched family of related objects that must vary togetherAbstract FactorySwapping the factory swaps the whole family, so mismatched pieces can't be combined.
An object with many optional parts, assembled step by stepBuilder (this lesson)Named, optional steps instead of long positional constructors; validate once in build().
Many near-identical objects that are expensive to set upPrototypeCopy a ready-made template instead of repeating costly construction.

Common mistakes

MistakeWhat happensDo this instead
A builder for one or two required fieldsceremony with no benefita constructor or a record / dataclass
Validating in each step methodcross-field rules can't be checkedvalidate once in build()
Passing the builder's own list to the productbuilding again changes the earlier productcopy collections in build()
Sharing one builder between threadsinterleaved steps corrupt the productone builder per construction
(Python) Writing a builder where keyword arguments would doextra code for what HttpRequest(url, timeout_ms=5000) already giveskeyword arguments + __post_init__ validation

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. A User has an id and an email, both required, nothing optional. Builder?

Show answer

No - a constructor or a record is clearer. A Builder pays off when there are several optional parts.

2. Why validate in build() rather than in each step method?

Show answer

Steps can run in any order, so only build() sees the final value of every field. Rules across fields (no body on a GET) can only be checked there - once - before an immutable, valid object is created.

3. Two threads share one builder and both call addTopping(...). What's wrong?

Show answer

A builder is mutable, so sharing it races like any other mutable object. Use one builder per construction, confined to one thread.

Key takeaways

  • Reach for it when an object has several optional parameters; one or two required fields just need a constructor.
  • Each step is named and optional, so calls read like a sentence and avoid positional-argument mistakes.
  • Validate everything once in build(), then return an immutable object.
  • Builder methods return this (self in Python) so the steps can be chained fluently.
  • Treat a builder as single-use and thread-confined - never share one between threads.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You needUse
A few required fieldsa constructor, record or dataclass
Many optional parts (Java)a fluent builder with build()
Many optional parts (Python)keyword arguments with defaults + __post_init__
Required steps checked at compile timea staged builder
Reusable, shareable partial objectsan immutable builder (each step returns a new one)
One construction process, many output formatsthe GoF Builder with a director

Snippets worth knowing by heart

Java

static class Builder {
    private String method = "GET";
    private String url, body;
    private final List<String> headers = new ArrayList<>();

    Builder url(String u) { this.url = u; return this; }
    Builder header(String h) { headers.add(h); return this; }

    HttpRequest build() {
        if (url == null) throw new IllegalArgumentException("url is required");
        if (body != null && method.equals("GET")) throw new IllegalArgumentException("GET request cannot have a body");
        return new HttpRequest(method, url, body, List.copyOf(headers));   // copy on the way out
    }
}

Python

@dataclass(frozen=True)
class HttpRequest:
    url: str
    method: str = "GET"
    body: str | None = None
    timeout_ms: int = 30_000

    def __post_init__(self):
        if self.body is not None and self.method == "GET":
            raise ValueError("GET request cannot have a body")

r = HttpRequest("https://api.example.com/users", timeout_ms=5_000)

The traps

  • A builder for two required fields - ceremony.
  • Validating per step - cross-field rules need build().
  • Handing over the builder's own collections - later builds change earlier products.
  • Sharing a builder between threads.

Staff-level points, one line each

  • GoF Builder = a director driving one process into many representations; Bloch's = named optional parameters.
  • Building twice without copying gave both pizzas [cheese, olives]; List.copyOf / tuple fixed it.
  • Python can't force build(): a dataclass built directly skipped validation - rules belong in __post_init__.
  • Staged builders make a missing step a compile error (method not found on NeedsUrl).
  • Hierarchy chains lose the subtype (thinCrust() not found) - generic self type and self(); -> Self in Python.
  • Immutable builders (SQLAlchemy select()) make every step a shareable, thread-safe template.
  • "Copy with one change": dataclasses.replace, copy.replace, withX() methods.

Say it in an interview

  • "It has a dozen optional fields, so I'd use a builder and validate once in build()."
  • "build() copies the headers, so reusing the builder can't change requests already built."
  • "In Python I'd use keyword arguments and validate in __post_init__ - no builder needed."
  • "Required steps are separate interfaces, so a missing URL doesn't compile."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Builder Pattern free

Price a Pizza via the Builder Pattern

MediumProCoding problemJava · Python

Implement PizzaBuilder (with chainable setSize/addTopping, and a build() that produces a Pizza) and Pizza.price(). Base prices: SMALL = 5.00, MEDIUM = 7.00, LARGE = 9.00. Each topping adds 1.50.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Build Valid, Immutable HTTP Requests

MediumProCoding problemJava · Python

HttpRequest is immutable: all its fields are final and its constructor is private. The only way to make one is through its nested Builder. Finish the builder.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Builder Pattern free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime