Builder Pattern

Introduction

The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It shines when an object requires many configuration options, optional parameters, or a specific construction sequence — making the code readable and maintainable where telescoping constructors would fail.

The Problem

Consider building an HTTP request object. You need a URL, an HTTP method, headers, query parameters, a request body, a timeout value, and retry settings. A constructor with all these parameters is unreadable and error-prone. You cannot tell which argument is which without checking the signature, and adding new options means changing every call site. Optional parameters make this even messier.

The Solution

The Builder pattern provides a step-by-step API for constructing the object. Each method sets one property and returns the builder itself, enabling method chaining. When all desired options are configured, a final build() call produces the immutable result. The object is always in a valid, complete state after construction.

Key Principle

Separate construction from representation. The builder encapsulates the construction logic so the client code reads like a declaration of what the object should look like, not how it gets assembled internally.

Java Example

This example builds an HttpRequest object with a fluent API, supporting method, URL, headers, body, timeout, and retries.

import java.util.Collections;
import java.util.HashMap;
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 timeoutMs;
    private final int maxRetries;

    // Private constructor — only the Builder can create instances
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Collections.unmodifiableMap(builder.headers);
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
        this.maxRetries = builder.maxRetries;
    }

    public String getUrl() { return url; }
    public String getMethod() { return method; }
    public Map<String, String> getHeaders() { return headers; }
    public String getBody() { return body; }
    public int getTimeoutMs() { return timeoutMs; }
    public int getMaxRetries() { return maxRetries; }

    @Override
    public String toString() {
        return method + " " + url
            + " | headers=" + headers.size()
            + " | timeout=" + timeoutMs + "ms"
            + " | retries=" + maxRetries;
    }

    // Static inner Builder class
    public static class Builder {
        private final String url;       // Required
        private String method = "GET";  // Default
        private Map<String, String> headers = new HashMap<>();
        private String body = null;
        private int timeoutMs = 30000;  // Default 30s
        private int maxRetries = 0;

        public Builder(String url) {
            if (url == null || url.isEmpty()) {
                throw new IllegalArgumentException("URL is required");
            }
            this.url = url;
        }

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

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

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

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

        public Builder retries(int maxRetries) {
            this.maxRetries = maxRetries;
            return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }
}

// Usage
public class Application {
    public static void main(String[] args) {
        HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
            .method("POST")
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer token123")
            .body("{\"name\": \"Alice\", \"email\": \"alice@example.com\"}")
            .timeout(5000)
            .retries(3)
            .build();

        System.out.println(request);

        // Simple GET request with defaults
        HttpRequest getRequest = new HttpRequest.Builder("https://api.example.com/health")
            .build();

        System.out.println(getRequest);
    }
}

Python Example

The same HTTP request builder in Python, using method chaining and an immutable result.

from dataclasses import dataclass, field
from typing import Optional


@dataclass(frozen=True)
class HttpRequest:
    url: str
    method: str = "GET"
    headers: dict = field(default_factory=dict)
    body: Optional[str] = None
    timeout_ms: int = 30000
    max_retries: int = 0

    def __str__(self):
        return (
            f"{self.method} {self.url}"
            f" | headers={len(self.headers)}"
            f" | timeout={self.timeout_ms}ms"
            f" | retries={self.max_retries}"
        )


class HttpRequestBuilder:
    def __init__(self, url: str):
        if not url:
            raise ValueError("URL is required")
        self._url = url
        self._method = "GET"
        self._headers = {}
        self._body = None
        self._timeout_ms = 30000
        self._max_retries = 0

    def method(self, method: str) -> "HttpRequestBuilder":
        self._method = method
        return self

    def header(self, key: str, value: str) -> "HttpRequestBuilder":
        self._headers[key] = value
        return self

    def body(self, body: str) -> "HttpRequestBuilder":
        self._body = body
        return self

    def timeout(self, timeout_ms: int) -> "HttpRequestBuilder":
        self._timeout_ms = timeout_ms
        return self

    def retries(self, max_retries: int) -> "HttpRequestBuilder":
        self._max_retries = max_retries
        return self

    def build(self) -> HttpRequest:
        return HttpRequest(
            url=self._url,
            method=self._method,
            headers=dict(self._headers),
            body=self._body,
            timeout_ms=self._timeout_ms,
            max_retries=self._max_retries,
        )


# Usage
if __name__ == "__main__":
    request = (
        HttpRequestBuilder("https://api.example.com/users")
        .method("POST")
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer token123")
        .body('{"name": "Alice", "email": "alice@example.com"}')
        .timeout(5000)
        .retries(3)
        .build()
    )

    print(request)

    # Simple GET with defaults
    get_request = HttpRequestBuilder("https://api.example.com/health").build()
    print(get_request)

When to Use

  • Objects with many optional parameters — when a constructor would need 5+ parameters and most are optional, a builder makes the code self-documenting.
  • Immutable objects — when you want the final object to be immutable but need flexibility during construction. The builder accumulates state, then produces a frozen result.
  • Complex configuration — building SDK clients, HTTP requests, database queries, or email messages where the construction involves many steps and validation.
  • Test data generation — creating test fixtures with sensible defaults that can be selectively overridden per test case.

Real-World Usage

  • Java StringBuilder — builds strings efficiently through repeated append() calls before producing the final String.
  • OkHttp Request.Builder — constructs HTTP requests with a fluent API for URL, method, headers, and body.
  • Lombok @Builder — auto-generates builder classes for Java POJOs at compile time.
  • Python requests libraryrequests.Request objects can be prepared step-by-step before sending via a Session.
  • Protocol Buffers — all protobuf message classes use builders for construction in both Java and C++.



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *