Free Interactive Course · Design Patterns

Builder Design Pattern

Assemble a complicated object step by step, instead of through a constructor nobody can read.

Creational Patternseasy
ShareXLinkedIn
In one sentence

Separate the construction of a complex object from its representation, so the same process can build different results.

01

The problem Builder solves

The problem

You need an HTTP request object. It has a URL and a method — easy. Then headers. Then a timeout. Then retries, a body, query parameters, whether to follow redirects, and an idempotency key.

Now the constructor is new HttpRequest(url, "POST", headers, 30, 3, body, null, true, null) and nobody reading that call site can tell you what 30, 3 and true mean without opening the class. Worse, most callers want two of those nine things, so you write six overloads — the telescoping constructor — and every new option doubles the pile.

The setter alternative is no better: new HttpRequest() followed by eight assignments means the object exists in an invalid, half-built state that some other thread or an early return can observe.

02

How the Builder pattern works

Give construction its own object, and make it read like a sentence:

  1. A builder collects the pieces one call at a time. Each call names its argument, so .timeout(30) replaces a bare 30.
  2. Each step returns the builder, so calls chain. Order stops mattering and you only mention what you care about.
  3. A final build() validates everything and returns the finished object — immutable, and never seen half-assembled.
The GoF book also describes a Director, which drives a builder through a fixed recipe so the same steps can produce different representations. In modern code it's usually skipped — a static factory like HttpRequest.json(url) that returns a preconfigured builder covers the same ground with less machinery.
ClientHttpRequestBuilder+ header(k,v) : this+ timeout(s) : this+ build() : HttpRequesteach stepreturns itselfHttpRequestimmutablethe half-built state lives in the builder, never in the product
Participants. The Builder holds mutable, partially-complete state. The Product is created once, complete, at build(). The client never holds a half-configured request, which is the guarantee that setters can't give you.
03

See it: build a request piece by piece

Add and remove options. Watch the expression at the bottom — that's the code you'd write, and it only ever mentions what you actually chose.

▶ Try it — chain the steps

An interactive builder: adding options such as headers, a timeout or retries appends a named step to a fluent call chain, and the resulting expression only mentions the options you chose.

04

Builder pattern code examples

A fluent, validating builder for an immutable request object.

public final class HttpRequest {
    private final String url, method;
    private final Map<String, String> headers;
    private final Duration timeout;
    private final int retries;

    // Only the builder can call this, so a half-built request cannot exist.
    private HttpRequest(Builder b) {
        this.url = b.url;
        this.method = b.method;
        this.headers = Map.copyOf(b.headers);   // defensive copy: truly immutable
        this.timeout = b.timeout;
        this.retries = b.retries;
    }

    public static Builder post(String url) { return new Builder(url, "POST"); }

    public static final class Builder {
        private final String url, method;
        private final Map<String, String> headers = new LinkedHashMap<>();
        private Duration timeout = Duration.ofSeconds(10);   // sensible defaults
        private int retries = 0;

        private Builder(String url, String method) { this.url = url; this.method = method; }

        public Builder header(String k, String v) { headers.put(k, v); return this; }
        public Builder timeout(Duration d) { this.timeout = d; return this; }
        public Builder retries(int n) { this.retries = n; return this; }

        public HttpRequest build() {
            // One place to enforce cross-field rules the constructor couldn't.
            if (retries > 0 && !"GET".equals(method) && !headers.containsKey("Idempotency-Key"))
                throw new IllegalStateException("retrying a non-GET needs an idempotency key");
            return new HttpRequest(this);
        }
    }
}

// HttpRequest.post("/v1/orders").header("Content-Type", "application/json")
//            .timeout(Duration.ofSeconds(30)).build();
Read across the tabs: Java, C++ and C# need the builder class because their constructors can't take named optional arguments. Python has keyword arguments, C# has init + with, and Go has functional options — so in those languages a builder only pays for itself when steps accumulate (many .header() calls) or when validation must happen once at the end. TypeScript is the interesting one: it can encode "you may not call build() yet" in the type system.
05

How to implement Builder

  1. Make the product's fields final/readonly and its constructor private to the builder — that is what makes the finished object trustworthy.
  2. Give the builder one mutable field per product field, with sensible defaults so callers only mention what differs.
  3. Have each step return the builder itself so calls chain. Name steps after the concept (.timeout(30)), never after the type.
  4. Put cross-field validation in build(), where every value is finally known. This is the thing a pile of setters cannot do.
  5. Return a defensive copy of any collection you were handed, or the caller keeps a reference into your "immutable" object.
  6. Offer static entry points for common shapes — HttpRequest.post(url) — instead of a Director class.
06

When to use Builder — and when not to

Use it when a constructor has grown past roughly four parameters, when several of them are optional, when adjacent parameters share a type (so (30, 3) can be silently swapped), or when the object should be immutable but is fiddly to assemble.

Where it goes wrong

Builders for three-field objects. You've doubled the class count to avoid a constructor that was already readable. Four parameters is a reasonable threshold; two is not.

A builder that never validates. If build() just copies fields across, it is a more verbose set of setters. The end-of-build check is most of the value.

Reusing a builder after build(). Unless you deliberately snapshot, a second build() can hand out an object that shares mutable state with the first. Either copy defensively or make build() consume the builder, as the C++ tab does with &&.

Writing one in a language that doesn't need it. In Python or Go, keyword arguments and functional options already give you named optional construction.

07

Quick check

🧠 Quick check
What can a Builder do that a no-arg constructor plus a set of setters cannot?

In the wild

Javajava.net.http.HttpRequest.newBuilder() in the JDK, plus StringBuilder and OkHttp's Request.Builder.
C#WebApplication.CreateBuilder(args) — the whole ASP.NET Core startup is a builder, and StringBuilder is the oldest example in the framework.
PythonQuery builders such as SQLAlchemy's select(...).where(...).order_by(...), where each step returns a new statement object.
Gostrings.Builder in the standard library, and the functional-options style used across grpc-go and the AWS SDK.
TypeScriptPrisma and Knex query builders — chained, typed steps producing one executable query.
C++std::ostringstream accumulating output, and the fluent builders in Protocol Buffers' generated C++ API.

Frequently asked questions

When should I use the Builder pattern?
When a constructor has grown past about four parameters, when several are optional, when neighbouring parameters share a type and could be swapped without a compile error, or when you want the object to be immutable but it takes several steps to assemble. Below that bar, a plain constructor is clearer and a builder is just more code.
What is the difference between Builder and Factory?
A factory answers "which class should I create?" and returns the object immediately. A builder answers "how do I assemble this one complicated object?" and returns nothing until you call build(). They compose happily: a factory can return a preconfigured builder.
Do I need a Director class?
Almost never in modern code. The Director exists in the GoF version to drive a builder through a fixed sequence so the same steps can produce different representations. In practice a static factory that returns a preconfigured builder — HttpRequest.json(url) — is easier to read and to discover.
Is the Builder pattern thread-safe?
The builder itself is mutable and generally is not — share one across threads and you'll get interleaved steps. The product is the thread-safe part, provided you made it genuinely immutable, including defensive copies of any collections. The normal usage is one builder per thread, per call.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.