Builder Pattern explained simply
Thilan Dissanayaka Software Architecture February 05, 2020

Builder Pattern explained simply

You have probably written a constructor that takes six, seven, maybe ten parameters. And every time you call it, you are squinting at the argument list trying to remember if timeout comes before retryCount or after body. You swap two arguments, everything compiles fine because they are both integers, and then you spend an hour debugging why your request times out after 3 milliseconds with 5000 retries.

That is the telescoping constructor problem. And the Builder Pattern exists to kill it.

Let’s get into it.

What is the Builder Pattern?

  • Separates the construction of a complex object from its representation.
  • Lets you build an object step by step, setting only the fields you care about.
  • The final object is typically immutable – once built, it cannot be changed.

In short:

Build complex objects piece by piece, not all at once.

Real-Life Analogy

Think about building a custom PC. You don’t walk into a store and say “give me a computer with Intel-i7, 32GB, RTX-4070, 1TB-NVMe, 750W, ATX-mid-tower” all in one breath and hope the clerk gets the order right.

Instead, you pick components one at a time:

  • CPU: Intel i7
  • RAM: 32GB DDR5
  • GPU: RTX 4070
  • Storage: 1TB NVMe SSD
  • PSU: 750W
  • Case: ATX Mid Tower

Each choice is independent. You can skip the GPU if you are building a server. You can upgrade the RAM later in the process. And when you are done picking, the shop assembles the final machine. That assembled PC is your Product – immutable, sealed, ready to use.

That is exactly how the Builder Pattern works in code.

Class Diagram

Here is how the Builder Pattern looks when applied to building HTTP requests. The HttpRequestBuilder constructs the HttpRequest step by step using fluent method calls, and the .build() method produces the final immutable object.

Key Components

  • Builder (Interface): Declares the step-by-step methods for constructing the product. Each method returns the builder itself so you can chain calls.
  • ConcreteBuilder (HttpRequestBuilder): Implements the builder interface. Stores the configuration internally and assembles the final product when build() is called.
  • Product (HttpRequest): The complex object being constructed. Once built, it is immutable – no setters, no surprises.
  • Client: The code that uses the builder. It only interacts with the builder’s fluent API and never touches the product’s constructor directly.

The important thing here is that the client code reads like a sentence. You can look at a builder chain and immediately understand what is being configured.

Example in Java – Building HTTP Requests

This is the pattern behind Java’s HttpRequest.Builder, OkHttp’s Request.Builder, and pretty much every HTTP client library. Let’s build a simplified version from scratch.

HttpRequest Class (Product)

import java.util.Collections;
import java.util.Map;

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

    // package-private constructor -- only the builder can call this
    HttpRequest(String url, String method, Map<String, String> headers,
                String body, int timeout, int retryCount) {
        this.url = url;
        this.method = method;
        this.headers = Collections.unmodifiableMap(headers);
        this.body = body;
        this.timeout = timeout;
        this.retryCount = retryCount;
    }

    public String getUrl() { return url; }
    public String getMethod() { return method; }
    public Map<String, String> getHeaders() { return headers; }
    public String getBody() { return body; }
    public int getTimeout() { return timeout; }
    public int getRetryCount() { return retryCount; }

    @Override
    public String toString() {
        return method + " " + url + "\n"
            + "Headers: " + headers + "\n"
            + "Body: " + body + "\n"
            + "Timeout: " + timeout + "ms | Retries: " + retryCount;
    }
}

Notice there are no setters. Once the HttpRequest is built, it is locked down. The headers map is wrapped with Collections.unmodifiableMap() so nobody can sneak in modifications after construction. This is exactly how you want request objects to behave – immutable and thread-safe.

HttpRequestBuilder Class (ConcreteBuilder)

import java.util.HashMap;
import java.util.Map;

public class HttpRequestBuilder {
    private String url;
    private String method = "GET";
    private final Map<String, String> headers = new HashMap<>();
    private String body;
    private int timeout = 30000;
    private int retryCount = 0;

    public HttpRequestBuilder url(String url) {
        this.url = url;
        return this;
    }

    public HttpRequestBuilder method(String method) {
        this.method = method;
        return this;
    }

    public HttpRequestBuilder header(String key, String value) {
        this.headers.put(key, value);
        return this;
    }

    public HttpRequestBuilder body(String body) {
        this.body = body;
        return this;
    }

    public HttpRequestBuilder timeout(int timeoutMs) {
        this.timeout = timeoutMs;
        return this;
    }

    public HttpRequestBuilder retries(int count) {
        this.retryCount = count;
        return this;
    }

    public HttpRequest build() {
        if (url == null || url.isEmpty()) {
            throw new IllegalStateException("URL is required");
        }
        return new HttpRequest(url, method, headers, body, timeout, retryCount);
    }
}

So what’s going on here? Every setter method returns this – the builder itself. That is what enables method chaining. The build() method does a quick validation check (you can’t make an HTTP request without a URL) and then hands everything off to the HttpRequest constructor.

Notice the sensible defaults too. Method defaults to GET. Timeout defaults to 30 seconds. Retry count defaults to zero. You only override what you need.

Putting It All Together

public class MainProgram {
    public static void main(String[] args) {
        String jsonPayload = "{\"username\":\"alice\",\"role\":\"admin\"}";

        HttpRequest request = new HttpRequestBuilder()
            .url("https://api.example.com/users")
            .method("POST")
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer token")
            .body(jsonPayload)
            .timeout(5000)
            .retries(3)
            .build();

        System.out.println(request);
        System.out.println("---");

        // simple GET request -- most defaults are fine
        HttpRequest simpleGet = new HttpRequestBuilder()
            .url("https://api.example.com/health")
            .build();

        System.out.println(simpleGet);
    }
}

Output:

POST https://api.example.com/users
Headers: {Content-Type=application/json, Authorization=Bearer token}
Body: {"username":"alice","role":"admin"}
Timeout: 5000ms | Retries: 3
---
GET https://api.example.com/health
Headers: {}
Body: null
Timeout: 30000ms | Retries: 0

Compare that to what the constructor call would look like without the builder:

HttpRequest request = new HttpRequest(
    "https://api.example.com/users", "POST", headers,
    jsonPayload, 5000, 3
);

Quick – is 5000 the timeout or the retry count? With the builder, you never have to ask that question.

When to Use the Builder Pattern

  • When your class has many constructor parameters (especially optional ones).
  • When you want the constructed object to be immutable.
  • When the order of construction steps matters or you want to enforce validation before building.
  • When you want your object creation code to be readable – builder chains document themselves.

Advantages and Disadvantages

Advantages:

  • Eliminates telescoping constructors – no more guessing which parameter is which.
  • Makes code self-documenting. .timeout(5000) is infinitely clearer than a naked 5000 in an argument list.
  • Enforces immutability on the product. Once built, the object is safe to share across threads.
  • Optional parameters get sensible defaults. You only set what you need.
  • Validation happens at build time, not scattered across setters.

Disadvantages:

  • More classes to write. For a simple object with two or three fields, a builder is overkill.
  • Code duplication between the builder and the product (same fields in both places).
  • Slightly more memory overhead since you are creating a builder object that gets thrown away after build().

Real-World Use Cases

  • Java’s StringBuilder: The original builder. Append strings piece by piece, then call toString() to get the final immutable String.
  • OkHttp Request.Builder: Every Android and Java developer who has made an HTTP call has used this. .url(), .header(), .post(), .build().
  • Lombok @Builder: Generates the entire builder class for you with a single annotation. If you are writing Java and not using this, you are writing too much boilerplate.
  • Protocol Buffers: Google’s protobuf generates builder classes for every message type. Message.newBuilder().setField(value).build().
  • Java’s HttpRequest.Builder (Java 11+): The standard library adopted this pattern for java.net.http.HttpRequest. The JDK team themselves decided builders are the right approach for complex request objects.
  • Spring Security Configuration: If you have worked with Spring Security, you have seen builder chains like http.csrf().disable().authorizeRequests().antMatchers(...) – same pattern, applied to security config.

Wrapping Up

The Builder Pattern solves a real problem that every developer hits eventually – constructors that have too many parameters. Instead of a wall of arguments where you pray the order is correct, you get a fluent API that reads like plain English.

The rule of thumb is simple: if your constructor has more than three or four parameters, or if several of them are optional, reach for a builder. Your future self debugging that code at 2 AM will thank you.

ALSO READ
Writing a Shell Code for Linux
Apr 21, 2020 Exploit Development

Shellcode is a small piece of machine code used as the payload in exploit development. In this post, we write Linux shellcode from scratch — starting with a simple exit, building up to spawning a shell, and explaining every decision along the way.

Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Common Web Application Attacks
Feb 05, 2020 Application Security

Web applications are one of the most targeted surfaces by attackers. This is primarily because they are accessible over the internet, making them exposed and potentially vulnerable. Since these...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

> > >