LESSON 4.5
Builder
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
14 min read
Real-world analogy
Visit a custom sandwich shop or gourmet burger counter. You don't have a single giant menu button for every possible permutation of 15 toppings, 4 cheeses, 3 buns, and 6 sauces (which would require thousands of combinations).
Instead, the counter attendant follows a Builder process: they start with a base bun, prompt you step-by-step for meat, cheese, toppings, and condiments, and only hand you the final wrapped meal once construction is verified and complete. If you want a standard preset (like the House Double Bacon), the head chef acts as a Director, instructing the builder to execute a predefined sequence of steps.
The problem
When constructing domain objects with many configuration parameters—especially when several fields are optional or share identical primitive types—constructors become an unmaintainable hazard. This code smell is known as telescoping constructors.
The naive code below shows how easy it is to introduce bugs through ambiguous argument order:
class HttpRequest {
// Telescoping constructor with positional parameters:
constructor(url, method = "GET", headers = {}, body = null, timeout = 5000, followRedirects = true, retryCount = 0) {
this.url = url;
this.method = method;
this.headers = headers;
this.body = body;
this.timeout = timeout;
this.followRedirects = followRedirects;
this.retryCount = retryCount;
}
}
// What do these booleans and numbers mean? Easy to swap timeout and retryCount!
const req = new HttpRequest("https://api.example.com", "POST", {}, null, 3000, false, 3);
console.log(`URL: ${req.url}, Method: ${req.method}, Timeout: ${req.timeout}`);import java.util.Map;
public class Example {
static class HttpRequest {
private final String url;
private final String method;
private final Map<String, String> headers;
private final String body;
private final int timeout;
The pain is critical:
- Callers must pass
nullor default flags for unused intermediate arguments. - Swapping adjacent integer arguments (like
timeoutandretryCount) compiles without error but causes silent runtime failures. - Adding a new optional field requires creating another overloaded constructor or modifying all existing invocations.
The solution
Intent: Separate the construction of a complex object from its representation so that the same construction process can create different representations.
The participants are:
- Builder: Specifies an abstract interface for creating parts of a Product object.
- ConcreteBuilder: Implements the Builder interface, maintains internal state, constructs product components, and provides a method to retrieve the finished product (
build()). - Product: The complex object being constructed, typically with private fields and no public mutators (immutable).
- Director (Optional): Constructs an object using the Builder interface, encapsulating standard recipes.
Step-by-step implementation
Step 1: Create the immutable Product with private constructor
Make the product's fields readonly (or private in JS) and ensure only its builder can instantiate it.
Step 2: Implement the Builder with fluent chaining
Add setter methods that set builder fields and return this to allow fluent method chaining.
Step 3: Enforce validation in build()
The build() method verifies that mandatory fields are present and that all cross-field constraints hold before instantiating the product.
The complete example
Here is a runnable HTTP client request builder with fluent configuration, validation, and a Director that produces standard health check queries.
POST https://api.internal/v1/orders (timeout: 2500ms) [AUTH]
GET https://api.internal/health (timeout: 1000ms)Variations
1. Fluent Builder
The most common variant in modern software. Chained methods return this or a typed builder instance, allowing readable construction syntax.
2. Director pattern
A separate orchestrator that encapsulates recipes for constructing standard representations (such as StandardGamingPC or OfficePC). The director receives the builder interface and calls its steps in order.
3. Step Builder (Wizard Builder)
Enforces mandatory parameters in a compile-time sequence before optional ones:
// Interfaces enforce: UrlStep -> MethodStep -> OptionalStep -> build()
public interface UrlStep { MethodStep url(String url); }
public interface MethodStep { OptionalStep method(String method); }
public interface OptionalStep {
OptionalStep timeout(int ms);
HttpRequest build();
}- Pros: A caller cannot invoke
.build()until all mandatory steps have been supplied in order.
4. Telescoping constructors vs Builder
- Telescoping constructors scale as $O(N)$ constructors for $N$ parameters and become completely unusable beyond 4–5 arguments.
- Builder scales gracefully to 20+ parameters with zero ambiguity and full immutability.
5. Lombok @Builder in Java
In modern Java enterprise development, the Project Lombok library generates the private constructor, static inner builder class, and fluent setters automatically at compile time via @Builder. While clean, understanding the underlying pattern is essential for implementing custom validation in build().
Where you've already used it
- Java Standard Library (JDK):
java.lang.StringBuilder/StringBuffer: Builds variable-length strings efficiently across append steps.java.net.http.HttpRequest.newBuilder(): Modern Java 11+ HTTP client request builder with fluent headers, timeouts, and body publishers.java.nio.file.attribute.AclEntry.newBuilder(): Builds filesystem access control entries.
- Node.js and npm ecosystem:
- Database query builders like Knex.js and Prisma (
knex.select().from().where()). - Web framework configuration in Fastify or Express middleware chains.
- Database query builders like Knex.js and Prisma (
- Frontend & React:
- Fluent state machine definition libraries like XState.
Pros and cons
| Strengths | Weaknesses |
|---|---|
| Eliminates error-prone telescoping constructors | Requires creating a duplicate set of builder fields |
| Guarantees true immutability of the final product | Generates extra class boilerplate (unless using Lombok) |
Supports rich validation and invariants inside build() | Increases memory overhead slightly with transient builder instances |
| Makes client construction code self-documenting | Overkill for simple domain models with only 2–3 fields |
When not to use it
Do not use Builder for small value objects with 1 to 3 fields where a constructor or simple factory function is immediately clear. Builder is designed for complex objects with many optional fields or strict construction sequences.
Related patterns
| Pattern | Relationship to Builder |
|---|---|
| Abstract Factory | Abstract Factory creates families of related products in a single step; Builder creates a single complex product in multiple steps. |
| Factory Method | Factory Method defers creation to subclasses; Builder defers completion until all desired steps have executed. |
| Composite | Builders are often used to construct recursive Composite trees (like UI widget trees or AST nodes). |
| Singleton | Builders can return singletons or pool references, though typically they construct fresh immutable instances. |
Builder: SQL query generator
Refactor a telescoping query generator with 7 positional arguments into a fluent, validated Builder pattern that returns an immutable, frozen query object.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
Q1: How does Builder ensure immutability compared to JavaBean getters and setters?
JavaBeans require a no-arg constructor followed by public setters, leaving the object mutable and temporarily invalid while setters are being called across multiple lines. Builder collects parameters in a mutable builder, validates them, and passes them to a private constructor, producing an object with only final getters and no public mutators.
Q2: What is the purpose of the Director in the classic GoF Builder pattern?
The Director encapsulates reusable construction algorithms so clients do not have to repeat the exact sequence of builder method calls. Different directors can use the same builder to produce different presets, or the same director can use different builders to produce different representations (e.g. HTML vs Markdown).
Q3: Where should validation logic live in a Builder?
Basic single-field validations (like checking non-negative timeout) can live in the individual setter methods for fast failure. Complex cross-field validation invariants (like requiring password when username is provided) must live inside the build() method right before the product instance is created.
Q4: How do you enforce mandatory parameters at compile time with Builder?
Using the Step Builder pattern: divide the builder into a sequence of single-method interfaces. Each step's method returns the interface for the next mandatory step, and only the final step exposes optional setters and the build() method.
Try it
Configuration Loader LLD
Merge configuration sources with provenance, validation and atomic reload.