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 finalString. - 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 library —
requests.Requestobjects can be prepared step-by-step before sending via aSession. - Protocol Buffers — all protobuf message classes use builders for construction in both Java and C++.