Builder Design Pattern
1. What is Builder Pattern?
The Builder pattern is a GOF Creational Pattern. It creates a complex object by combining multiple simple objects step by step, separating construction from representation so that the same construction process can be used to create multiple objects of the same type.
One line: Build a complex object piece by piece using a step-by-step construction process.
Key concepts
- Complex object: The object creation that uses multiple other objects is called a complex object.
- Simple object: The object created directly for a class (created directly with
new) is called a simple object. - e.g., House is a complex object where Basement, Pillars, Roofing, and Interior are simple objects.
- e.g., Meal is a complex object where Rice, Curry, Chutney, and Pickle are simple objects.
Real-world examples
- House construction: A House is complex. It needs Basement, Pillars/Structure, Roofing, and Interior objects step by step. This construction process can be used to create different types of houses: MarbleHouse, BrickHouse, IceHouse (Igloo), WoodenHouse, etc.
- Indian Meal: A Meal is complex. It needs Rice, Curry, Roti, Curd, and Pickle objects step by step. This meal preparation process can be used to create different types: Veg Meal, NonVeg Meal, Andhra Meal, Gujarati Meal, Rajasthani Thali, etc.
- Computer: A Computer needs CPU, RAM, Storage, GPU, and PowerSupply. Different builders create Gaming PC, Office PC, Server, etc.
2. The Problem
Creating a complex class directly by defining the complex class is a costly affair for programmers. Problems include:
- Constructor with many parameters (telescoping constructor anti-pattern)
- Client must know the exact order and types of all parameters
- If a new part is added, every constructor call must change
- Cannot create partially built objects (some parts optional)
- No way to reuse the same construction process for different representations
Telescoping constructor problem
// Without Builder: constructor grows with every new field
public class House {
public House(Basement basement, Structure structure,
Roofing roofing, Interior interior,
Garden garden, Garage garage,
SwimmingPool pool, SecuritySystem security) {
// 8 parameters! Hard to read, easy to mix up order
}
}
// Client must pass ALL parameters, even optional ones
House house = new House(
new ConcreteBasement(),
new ConcreteStructure(),
new ConcreteRoofing(),
new ConcreteInterior(),
null, // no garden
null, // no garage
null, // no pool
null // no security
);
3. The Solution: Four Components
The Builder pattern separates object construction from its representation using four components:
| Component | Role | House Example |
|---|---|---|
| a) Product | The complex object being built | House |
| b) Builder (interface) | Declares the steps needed to build the product | HouseBuilder |
| c) ConcreteBuilder | Implements the builder for a specific product type | IglooHouseBuilder, ConcreteHouseBuilder, WoodenHouseBuilder |
| d) Director | Uses the builder to construct the product in a specific order. Like a CivilEngineer who uses the Builder support to create the product. | CivilEngineer |
4. Classic GOF Builder: House Construction
Simple object interfaces (parts of the house)
// Basement.java
package com.nt.product;
public interface Basement {
String getBasementType();
}
// Structure.java
package com.nt.product;
public interface Structure {
String getStructureType();
}
// Roofing.java
package com.nt.product;
public interface Roofing {
String getRoofingType();
}
// Interior.java
package com.nt.product;
public interface Interior {
String getInteriorType();
}
Concrete parts (one set per house type)
// Concrete house parts
public class ConcreteBasement implements Basement {
public String getBasementType() { return "Concrete Basement"; }
}
public class ConcreteStructure implements Structure {
public String getStructureType() { return "Concrete Pillars Structure"; }
}
public class ConcreteRoofing implements Roofing {
public String getRoofingType() { return "Concrete Slab Roofing"; }
}
public class ConcreteInterior implements Interior {
public String getInteriorType() { return "Concrete Interior with Paint"; }
}
// Wooden house parts
public class WoodenBasement implements Basement {
public String getBasementType() { return "Wooden Basement"; }
}
public class WoodenStructure implements Structure {
public String getStructureType() { return "Wooden Pillars Structure"; }
}
public class WoodenRoofing implements Roofing {
public String getRoofingType() { return "Wooden Roofing"; }
}
public class WoodenInterior implements Interior {
public String getInteriorType() { return "Wooden Panel Interior"; }
}
// Ice house (Igloo) parts
public class IceBasement implements Basement {
public String getBasementType() { return "Ice Basement"; }
}
public class IceStructure implements Structure {
public String getStructureType() { return "Ice Blocks Structure"; }
}
public class IceRoofing implements Roofing {
public String getRoofingType() { return "Ice Dome Roofing"; }
}
public class IceCurvingInterior implements Interior {
public String getInteriorType() { return "Ice Curving Interior"; }
}
House.java (the complex Product)
package com.nt.product;
public class House {
private Basement basement;
private Structure structure;
private Roofing roofing;
private Interior interior;
public void setBasement(Basement basement) { this.basement = basement; }
public void setStructure(Structure structure) { this.structure = structure; }
public void setRoofing(Roofing roofing) { this.roofing = roofing; }
public void setInterior(Interior interior) { this.interior = interior; }
public Basement getBasement() { return basement; }
public Structure getStructure() { return structure; }
public Roofing getRoofing() { return roofing; }
public Interior getInterior() { return interior; }
@Override
public String toString() {
return "House [\n"
+ " Basement = " + basement.getBasementType() + "\n"
+ " Structure = " + structure.getStructureType() + "\n"
+ " Roofing = " + roofing.getRoofingType() + "\n"
+ " Interior = " + interior.getInteriorType() + "\n"
+ "]";
}
}
HouseBuilder.java (Builder interface)
package com.nt.builder;
import com.nt.product.House;
public interface HouseBuilder {
void buildBasement();
void buildStructure();
void buildRoofing();
void buildInterior();
House getHouse();
}
ConcreteHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class ConcreteHouseBuilder implements HouseBuilder {
private House house;
public ConcreteHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new ConcreteBasement()); }
public void buildStructure() { house.setStructure(new ConcreteStructure()); }
public void buildRoofing() { house.setRoofing(new ConcreteRoofing()); }
public void buildInterior() { house.setInterior(new ConcreteInterior()); }
public House getHouse() { return this.house; }
}
WoodenHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class WoodenHouseBuilder implements HouseBuilder {
private House house;
public WoodenHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new WoodenBasement()); }
public void buildStructure() { house.setStructure(new WoodenStructure()); }
public void buildRoofing() { house.setRoofing(new WoodenRoofing()); }
public void buildInterior() { house.setInterior(new WoodenInterior()); }
public House getHouse() { return this.house; }
}
IglooHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class IglooHouseBuilder implements HouseBuilder {
private House house;
public IglooHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new IceBasement()); }
public void buildStructure() { house.setStructure(new IceStructure()); }
public void buildRoofing() { house.setRoofing(new IceRoofing()); }
public void buildInterior() { house.setInterior(new IceCurvingInterior()); }
public House getHouse() { return this.house; }
}
CivilEngineer.java (Director)
package com.nt.director;
import com.nt.builder.HouseBuilder;
import com.nt.product.House;
public class CivilEngineer {
private HouseBuilder builder;
public CivilEngineer(HouseBuilder builder) {
this.builder = builder;
}
// Director defines the ORDER of construction steps
public House constructHouse() {
builder.buildBasement();
builder.buildStructure();
builder.buildRoofing();
builder.buildInterior();
return builder.getHouse();
}
}
Client test
package com.nt.test;
import com.nt.builder.*;
import com.nt.director.CivilEngineer;
import com.nt.product.House;
public class BuilderDPTest {
public static void main(String[] args) {
// Build a Concrete House
HouseBuilder concreteBuilder = new ConcreteHouseBuilder();
CivilEngineer engineer1 = new CivilEngineer(concreteBuilder);
House concreteHouse = engineer1.constructHouse();
System.out.println("=== Concrete House ===");
System.out.println(concreteHouse);
// Build a Wooden House
HouseBuilder woodenBuilder = new WoodenHouseBuilder();
CivilEngineer engineer2 = new CivilEngineer(woodenBuilder);
House woodenHouse = engineer2.constructHouse();
System.out.println("=== Wooden House ===");
System.out.println(woodenHouse);
// Build an Igloo House
HouseBuilder iglooBuilder = new IglooHouseBuilder();
CivilEngineer engineer3 = new CivilEngineer(iglooBuilder);
House iglooHouse = engineer3.constructHouse();
System.out.println("=== Igloo House ===");
System.out.println(iglooHouse);
}
}
5. Modern Fluent Builder (Joshua Bloch Style)
The classic GOF Builder uses a separate Builder class, Director, and product. In modern Java (and in frameworks like Lombok, Immutables), the Fluent Builder is more common. It uses method chaining, returns this from each setter, and has a build() method that creates the final immutable object. No Director class needed.
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
// Private constructor: only Builder can create this
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
}
// Getters only, no setters (immutable)
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
// Static inner Builder class
public static class Builder {
// Required fields
private final String method;
private final String url;
// Optional fields with defaults
private Map<String, String> headers = new HashMap<>();
private String body = "";
private int timeoutMs = 5000;
// Constructor takes only required fields
public Builder(String method, String url) {
this.method = method;
this.url = url;
}
// Each setter returns 'this' for chaining
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;
}
// Build method creates the final immutable object
public HttpRequest build() {
return new HttpRequest(this);
}
}
@Override
public String toString() {
return method + " " + url + "\n"
+ "Headers: " + headers + "\n"
+ "Body: " + (body.isEmpty() ? "(empty)" : body) + "\n"
+ "Timeout: " + timeoutMs + "ms";
}
}
// Usage: clean, readable, no parameter order confusion
HttpRequest request = new HttpRequest.Builder("POST", "https://api.nbi.com/transfer")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"from\":\"SAV2001\",\"to\":\"CUR3001\",\"amount\":50000}")
.timeout(10000)
.build();
GOF Builder vs Fluent Builder
| Aspect | GOF Builder (Classic) | Fluent Builder (Modern) |
|---|---|---|
| Director | Separate class controls build order | No director, client chains methods |
| Builder location | Separate class/interface | Static inner class |
| Product mutability | Mutable (setters) | Immutable (private constructor) |
| Method chaining | No | Yes (return this) |
| Required vs optional | All steps called by director | Required in constructor, optional via methods |
| Use case | Multiple representations of same construction process | Objects with many optional parameters |
| JDK examples | DocumentBuilder | StringBuilder, Stream.Builder, Locale.Builder |
6. Pre-defined Builder Examples in Java
| Class | Builder Type | Usage |
|---|---|---|
StringBuilder | Fluent | new StringBuilder().append("hello").append(" ").append("world").toString() |
StringBuffer | Fluent | Thread-safe version of StringBuilder |
Stream.Builder | Fluent | Stream.builder().add(1).add(2).add(3).build() |
Locale.Builder | Fluent | new Locale.Builder().setLanguage("en").setRegion("US").build() |
Calendar.Builder | Fluent | new Calendar.Builder().setDate(2026,10,2).build() |
DocumentBuilderFactory | GOF | Creates DocumentBuilder to parse XML |
ProcessBuilder | Fluent | new ProcessBuilder("ls", "-la").directory(dir).start() |
7. Full-Length Industry Project: HTTP Client Request Builder
National Bank of India's backend services communicate with external APIs (payment gateways, credit bureaus, government services). Each HTTP request needs a method, URL, headers, query parameters, body, timeout, retry configuration, and authentication. Some requests need all fields, others need only a few. The team wants a clean, immutable, type-safe way to construct HTTP requests without a 15-parameter constructor.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
HttpRequest.java (Product + Fluent Builder)
package com.nbi.http;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final Map<String, String> queryParams;
private final String body;
private final int timeoutMs;
private final int maxRetries;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.maxRetries = builder.maxRetries;
}
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getQueryParams() { return queryParams; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
public int getMaxRetries() { return maxRetries; }
public String getFullUrl() {
if (queryParams.isEmpty()) return url;
String params = queryParams.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
return url + "?" + params;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(method).append(" ").append(getFullUrl()).append("\n");
headers.forEach((k, v) -> sb.append(" ").append(k).append(": ").append(v).append("\n"));
if (!body.isEmpty()) {
sb.append(" Body: ").append(body.length() > 80 ? body.substring(0, 80) + "..." : body).append("\n");
}
sb.append(" Timeout: ").append(timeoutMs).append("ms | Retries: ").append(maxRetries);
return sb.toString();
}
public static class Builder {
private final String method;
private final String url;
private Map<String, String> headers = new HashMap<>();
private Map<String, String> queryParams = new HashMap<>();
private String body = "";
private int timeoutMs = 5000;
private int maxRetries = 3;
public Builder(String method, String url) {
if (method == null || method.isEmpty()) {
throw new IllegalArgumentException("HTTP method is required");
}
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("URL is required");
}
this.method = method.toUpperCase();
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder queryParam(String key, String value) {
this.queryParams.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body != null ? body : "";
return this;
}
public Builder timeout(int timeoutMs) {
if (timeoutMs <= 0) {
throw new IllegalArgumentException("Timeout must be positive");
}
this.timeoutMs = timeoutMs;
return this;
}
public Builder retries(int maxRetries) {
if (maxRetries < 0) {
throw new IllegalArgumentException("Retries cannot be negative");
}
this.maxRetries = maxRetries;
return this;
}
// Convenience methods for common patterns
public Builder bearerToken(String token) {
return header("Authorization", "Bearer " + token);
}
public Builder contentTypeJson() {
return header("Content-Type", "application/json");
}
public Builder contentTypeForm() {
return header("Content-Type", "application/x-www-form-urlencoded");
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
HttpResponse.java
package com.nbi.http;
import java.util.Map;
public class HttpResponse {
private final int statusCode;
private final String body;
private final Map<String, String> headers;
public HttpResponse(int statusCode, String body, Map<String, String> headers) {
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
}
public int getStatusCode() { return statusCode; }
public String getBody() { return body; }
public Map<String, String> getHeaders() { return headers; }
public boolean isSuccess() { return statusCode >= 200 && statusCode < 300; }
@Override
public String toString() {
return "HTTP " + statusCode + (isSuccess() ? " OK" : " ERROR")
+ " | Body: " + (body.length() > 80 ? body.substring(0, 80) + "..." : body);
}
}
HttpClient.java
package com.nbi.http;
import java.util.Map;
public class HttpClient {
public HttpResponse send(HttpRequest request) {
System.out.println("--- Sending Request ---");
System.out.println(request);
System.out.println();
// Simulate network call based on URL
int statusCode = 200;
String responseBody;
if (request.getUrl().contains("/transfer")) {
responseBody = "{\"status\":\"success\",\"txnId\":\"NEFT123456\"}";
} else if (request.getUrl().contains("/credit-score")) {
responseBody = "{\"score\":780,\"rating\":\"excellent\"}";
} else if (request.getUrl().contains("/balance")) {
responseBody = "{\"accountNo\":\"SAV2001\",\"balance\":250000.00}";
} else if (request.getUrl().contains("/error")) {
statusCode = 500;
responseBody = "{\"error\":\"Internal Server Error\"}";
} else {
responseBody = "{\"message\":\"OK\"}";
}
HttpResponse response = new HttpResponse(statusCode, responseBody,
Map.of("Content-Type", "application/json"));
System.out.println("--- Response ---");
System.out.println(response);
System.out.println();
return response;
}
}
Application.java
package com.nbi.http;
public class Application {
public static void main(String[] args) {
HttpClient client = new HttpClient();
// Scenario 1: Simple GET request (minimal builder usage)
System.out.println("=== Scenario 1: Simple GET ===");
HttpRequest getBalance = new HttpRequest.Builder("GET", "https://api.nbi.com/balance")
.queryParam("accountNo", "SAV2001")
.bearerToken("jwt_token_abc123")
.build();
client.send(getBalance);
// Scenario 2: POST with JSON body (full builder usage)
System.out.println("=== Scenario 2: POST Transfer ===");
HttpRequest transfer = new HttpRequest.Builder("POST", "https://api.nbi.com/transfer")
.contentTypeJson()
.bearerToken("jwt_token_abc123")
.body("{\"from\":\"SAV2001\",\"to\":\"CUR3001\",\"amount\":50000}")
.timeout(10000)
.retries(5)
.build();
client.send(transfer);
// Scenario 3: GET with multiple query params
System.out.println("=== Scenario 3: Credit Score Check ===");
HttpRequest creditCheck = new HttpRequest.Builder("GET", "https://api.creditbureau.com/credit-score")
.queryParam("pan", "ABCDE1234F")
.queryParam("consent", "true")
.header("X-API-Key", "bureau_key_xyz")
.timeout(15000)
.build();
client.send(creditCheck);
// Scenario 4: Immutability verification
System.out.println("=== Scenario 4: Immutability Check ===");
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/test")
.header("Accept", "application/json")
.build();
try {
req.getHeaders().put("Injected", "value");
System.out.println("FAILED: headers are mutable");
} catch (UnsupportedOperationException e) {
System.out.println("PASSED: headers are immutable (UnsupportedOperationException)");
}
// Scenario 5: Error response
System.out.println("\n=== Scenario 5: Error Response ===");
HttpRequest errorReq = new HttpRequest.Builder("POST", "https://api.nbi.com/error")
.contentTypeJson()
.body("{\"bad\":\"data\"}")
.build();
HttpResponse errorResp = client.send(errorReq);
System.out.println("Success? " + errorResp.isSuccess());
// Scenario 6: Validation error
System.out.println("\n=== Scenario 6: Builder Validation ===");
try {
new HttpRequest.Builder("", "https://api.nbi.com/test").build();
} catch (IllegalArgumentException e) {
System.out.println("PASSED: " + e.getMessage());
}
try {
new HttpRequest.Builder("GET", "https://api.nbi.com/test")
.timeout(-1)
.build();
} catch (IllegalArgumentException e) {
System.out.println("PASSED: " + e.getMessage());
}
}
}
Expected Output
HttpRequestBuilderTest.java
package com.nbi.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HttpRequestBuilderTest {
@Test
void build_createsRequestWithRequiredFields() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/test").build();
assertEquals("GET", req.getMethod());
assertEquals("https://api.nbi.com/test", req.getUrl());
}
@Test
void build_methodIsUppercased() {
HttpRequest req = new HttpRequest.Builder("post", "https://api.nbi.com").build();
assertEquals("POST", req.getMethod());
}
@Test
void build_setsDefaultTimeout() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com").build();
assertEquals(5000, req.getTimeoutMs());
}
@Test
void build_setsDefaultRetries() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com").build();
assertEquals(3, req.getMaxRetries());
}
@Test
void build_overridesDefaults() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.timeout(10000)
.retries(5)
.build();
assertEquals(10000, req.getTimeoutMs());
assertEquals(5, req.getMaxRetries());
}
@Test
void build_addsHeaders() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.header("Accept", "application/json")
.header("X-Custom", "value")
.build();
assertEquals("application/json", req.getHeaders().get("Accept"));
assertEquals("value", req.getHeaders().get("X-Custom"));
}
@Test
void build_headersAreImmutable() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.header("Accept", "application/json")
.build();
assertThrows(UnsupportedOperationException.class,
() -> req.getHeaders().put("Injected", "value"));
}
@Test
void build_bearerTokenSetsAuthHeader() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.bearerToken("mytoken")
.build();
assertEquals("Bearer mytoken", req.getHeaders().get("Authorization"));
}
@Test
void build_contentTypeJsonSetsHeader() {
HttpRequest req = new HttpRequest.Builder("POST", "https://api.nbi.com")
.contentTypeJson()
.build();
assertEquals("application/json", req.getHeaders().get("Content-Type"));
}
@Test
void build_queryParamsInFullUrl() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/search")
.queryParam("q", "hashmap")
.queryParam("limit", "10")
.build();
assertTrue(req.getFullUrl().contains("q=hashmap"));
assertTrue(req.getFullUrl().contains("limit=10"));
}
@Test
void builder_throwsForNullMethod() {
assertThrows(IllegalArgumentException.class,
() -> new HttpRequest.Builder(null, "https://api.nbi.com"));
}
@Test
void builder_throwsForNegativeTimeout() {
assertThrows(IllegalArgumentException.class,
() -> new HttpRequest.Builder("GET", "https://api.nbi.com").timeout(-1));
}
}
Key observations from the project
- Immutable product:
HttpRequesthas onlyfinalfields and a private constructor. Once built, it cannot be modified. Headers and query params are wrapped inCollections.unmodifiableMap. - Required vs optional fields:
methodandurlare required (in the Builder constructor). Everything else is optional with sensible defaults (timeout=5000, retries=3). - Method chaining: Each builder method returns
this, enabling fluent construction:.header(...).body(...).timeout(...).build(). - Convenience methods:
bearerToken(),contentTypeJson(),contentTypeForm()wrap common patterns so the client does not need to know the exact header names. - Validation in Builder: Null/empty method and URL are caught in the Builder constructor. Negative timeout is caught in
timeout(). Errors are thrown beforebuild()creates the object. - Six scenarios tested: Simple GET, POST with body, query params, immutability, error response, validation error.
8. When to Use and When NOT to Use
When to use Builder
| Scenario | Example | Why Builder |
|---|---|---|
| Many constructor parameters | HTTP request, database query, report config | Avoids telescoping constructor. Each parameter is named and optional. |
| Immutable objects | Value objects, configuration, DTOs | Builder collects data, then creates a frozen immutable object. |
| Multiple representations | ConcreteHouse, WoodenHouse, IglooHouse | Same construction process, different product types. |
| Complex object with dependencies | Computer (CPU + RAM + GPU + Storage) | Builder ensures all parts are assembled correctly before use. |
| Step-by-step construction | Document builder, meal builder | Each step adds one part. The order can be fixed (GOF) or flexible (fluent). |
When NOT to use Builder
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Simple objects (1-3 fields) | Builder adds unnecessary code for trivial objects | Constructor or factory method |
| Mutable objects | If the object changes after creation, builder's immutability benefit is lost | Constructor + setters |
| Objects created once at startup | Builder's readability benefit is wasted on a single call | Constructor or config file |
9. Quick Reference Card
| Component | GOF Builder | Fluent Builder |
|---|---|---|
| Product | House (mutable, setters) | HttpRequest (immutable, final fields) |
| Builder | HouseBuilder interface, separate concrete builders | Static inner Builder class |
| Director | CivilEngineer (controls build order) | Not needed (client chains methods) |
| Creation | engineer.constructHouse() | new Builder(...).method().build() |
| Immutable? | No (product has setters) | Yes (private constructor, final fields) |
| JDK example | DocumentBuilderFactory | StringBuilder, Locale.Builder |
Builder Design Pattern in Java
1. What is Builder Pattern?
The Builder pattern is a GOF Creational Pattern. It creates a complex object by combining multiple simple objects step by step, separating construction from representation so that the same construction process can be used to create multiple objects of the same type.
One line: Build a complex object piece by piece using a step-by-step construction process.
Key concepts
- Complex object: The object creation that uses multiple other objects is called a complex object.
- Simple object: The object created directly for a class (created directly with
new) is called a simple object. - e.g., House is a complex object where Basement, Pillars, Roofing, and Interior are simple objects.
- e.g., Meal is a complex object where Rice, Curry, Chutney, and Pickle are simple objects.
Real-world examples
- House construction: A House is complex. It needs Basement, Pillars/Structure, Roofing, and Interior objects step by step. This construction process can be used to create different types of houses: MarbleHouse, BrickHouse, IceHouse (Igloo), WoodenHouse, etc.
- Indian Meal: A Meal is complex. It needs Rice, Curry, Roti, Curd, and Pickle objects step by step. This meal preparation process can be used to create different types: Veg Meal, NonVeg Meal, Andhra Meal, Gujarati Meal, Rajasthani Thali, etc.
- Computer: A Computer needs CPU, RAM, Storage, GPU, and PowerSupply. Different builders create Gaming PC, Office PC, Server, etc.
2. The Problem
Creating a complex class directly by defining the complex class is a costly affair for programmers. Problems include:
- Constructor with many parameters (telescoping constructor anti-pattern)
- Client must know the exact order and types of all parameters
- If a new part is added, every constructor call must change
- Cannot create partially built objects (some parts optional)
- No way to reuse the same construction process for different representations
Telescoping constructor problem
// Without Builder: constructor grows with every new field
public class House {
public House(Basement basement, Structure structure,
Roofing roofing, Interior interior,
Garden garden, Garage garage,
SwimmingPool pool, SecuritySystem security) {
// 8 parameters! Hard to read, easy to mix up order
}
}
// Client must pass ALL parameters, even optional ones
House house = new House(
new ConcreteBasement(),
new ConcreteStructure(),
new ConcreteRoofing(),
new ConcreteInterior(),
null, // no garden
null, // no garage
null, // no pool
null // no security
);
3. The Solution: Four Components
The Builder pattern separates object construction from its representation using four components:
| Component | Role | House Example |
|---|---|---|
| a) Product | The complex object being built | House |
| b) Builder (interface) | Declares the steps needed to build the product | HouseBuilder |
| c) ConcreteBuilder | Implements the builder for a specific product type | IglooHouseBuilder, ConcreteHouseBuilder, WoodenHouseBuilder |
| d) Director | Uses the builder to construct the product in a specific order. Like a CivilEngineer who uses the Builder support to create the product. | CivilEngineer |
4. Classic GOF Builder: House Construction
Simple object interfaces (parts of the house)
// Basement.java
package com.nt.product;
public interface Basement {
String getBasementType();
}
// Structure.java
package com.nt.product;
public interface Structure {
String getStructureType();
}
// Roofing.java
package com.nt.product;
public interface Roofing {
String getRoofingType();
}
// Interior.java
package com.nt.product;
public interface Interior {
String getInteriorType();
}
Concrete parts (one set per house type)
// Concrete house parts
public class ConcreteBasement implements Basement {
public String getBasementType() { return "Concrete Basement"; }
}
public class ConcreteStructure implements Structure {
public String getStructureType() { return "Concrete Pillars Structure"; }
}
public class ConcreteRoofing implements Roofing {
public String getRoofingType() { return "Concrete Slab Roofing"; }
}
public class ConcreteInterior implements Interior {
public String getInteriorType() { return "Concrete Interior with Paint"; }
}
// Wooden house parts
public class WoodenBasement implements Basement {
public String getBasementType() { return "Wooden Basement"; }
}
public class WoodenStructure implements Structure {
public String getStructureType() { return "Wooden Pillars Structure"; }
}
public class WoodenRoofing implements Roofing {
public String getRoofingType() { return "Wooden Roofing"; }
}
public class WoodenInterior implements Interior {
public String getInteriorType() { return "Wooden Panel Interior"; }
}
// Ice house (Igloo) parts
public class IceBasement implements Basement {
public String getBasementType() { return "Ice Basement"; }
}
public class IceStructure implements Structure {
public String getStructureType() { return "Ice Blocks Structure"; }
}
public class IceRoofing implements Roofing {
public String getRoofingType() { return "Ice Dome Roofing"; }
}
public class IceCurvingInterior implements Interior {
public String getInteriorType() { return "Ice Curving Interior"; }
}
House.java (the complex Product)
package com.nt.product;
public class House {
private Basement basement;
private Structure structure;
private Roofing roofing;
private Interior interior;
public void setBasement(Basement basement) { this.basement = basement; }
public void setStructure(Structure structure) { this.structure = structure; }
public void setRoofing(Roofing roofing) { this.roofing = roofing; }
public void setInterior(Interior interior) { this.interior = interior; }
public Basement getBasement() { return basement; }
public Structure getStructure() { return structure; }
public Roofing getRoofing() { return roofing; }
public Interior getInterior() { return interior; }
@Override
public String toString() {
return "House [\n"
+ " Basement = " + basement.getBasementType() + "\n"
+ " Structure = " + structure.getStructureType() + "\n"
+ " Roofing = " + roofing.getRoofingType() + "\n"
+ " Interior = " + interior.getInteriorType() + "\n"
+ "]";
}
}
HouseBuilder.java (Builder interface)
package com.nt.builder;
import com.nt.product.House;
public interface HouseBuilder {
void buildBasement();
void buildStructure();
void buildRoofing();
void buildInterior();
House getHouse();
}
ConcreteHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class ConcreteHouseBuilder implements HouseBuilder {
private House house;
public ConcreteHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new ConcreteBasement()); }
public void buildStructure() { house.setStructure(new ConcreteStructure()); }
public void buildRoofing() { house.setRoofing(new ConcreteRoofing()); }
public void buildInterior() { house.setInterior(new ConcreteInterior()); }
public House getHouse() { return this.house; }
}
WoodenHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class WoodenHouseBuilder implements HouseBuilder {
private House house;
public WoodenHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new WoodenBasement()); }
public void buildStructure() { house.setStructure(new WoodenStructure()); }
public void buildRoofing() { house.setRoofing(new WoodenRoofing()); }
public void buildInterior() { house.setInterior(new WoodenInterior()); }
public House getHouse() { return this.house; }
}
IglooHouseBuilder.java
package com.nt.builder;
import com.nt.product.*;
public class IglooHouseBuilder implements HouseBuilder {
private House house;
public IglooHouseBuilder() {
this.house = new House();
}
public void buildBasement() { house.setBasement(new IceBasement()); }
public void buildStructure() { house.setStructure(new IceStructure()); }
public void buildRoofing() { house.setRoofing(new IceRoofing()); }
public void buildInterior() { house.setInterior(new IceCurvingInterior()); }
public House getHouse() { return this.house; }
}
CivilEngineer.java (Director)
package com.nt.director;
import com.nt.builder.HouseBuilder;
import com.nt.product.House;
public class CivilEngineer {
private HouseBuilder builder;
public CivilEngineer(HouseBuilder builder) {
this.builder = builder;
}
// Director defines the ORDER of construction steps
public House constructHouse() {
builder.buildBasement();
builder.buildStructure();
builder.buildRoofing();
builder.buildInterior();
return builder.getHouse();
}
}
Client test
package com.nt.test;
import com.nt.builder.*;
import com.nt.director.CivilEngineer;
import com.nt.product.House;
public class BuilderDPTest {
public static void main(String[] args) {
// Build a Concrete House
HouseBuilder concreteBuilder = new ConcreteHouseBuilder();
CivilEngineer engineer1 = new CivilEngineer(concreteBuilder);
House concreteHouse = engineer1.constructHouse();
System.out.println("=== Concrete House ===");
System.out.println(concreteHouse);
// Build a Wooden House
HouseBuilder woodenBuilder = new WoodenHouseBuilder();
CivilEngineer engineer2 = new CivilEngineer(woodenBuilder);
House woodenHouse = engineer2.constructHouse();
System.out.println("=== Wooden House ===");
System.out.println(woodenHouse);
// Build an Igloo House
HouseBuilder iglooBuilder = new IglooHouseBuilder();
CivilEngineer engineer3 = new CivilEngineer(iglooBuilder);
House iglooHouse = engineer3.constructHouse();
System.out.println("=== Igloo House ===");
System.out.println(iglooHouse);
}
}
5. Modern Fluent Builder (Joshua Bloch Style)
The classic GOF Builder uses a separate Builder class, Director, and product. In modern Java (and in frameworks like Lombok, Immutables), the Fluent Builder is more common. It uses method chaining, returns this from each setter, and has a build() method that creates the final immutable object. No Director class needed.
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final int timeoutMs;
// Private constructor: only Builder can create this
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(builder.headers);
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
}
// Getters only, no setters (immutable)
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
// Static inner Builder class
public static class Builder {
// Required fields
private final String method;
private final String url;
// Optional fields with defaults
private Map<String, String> headers = new HashMap<>();
private String body = "";
private int timeoutMs = 5000;
// Constructor takes only required fields
public Builder(String method, String url) {
this.method = method;
this.url = url;
}
// Each setter returns 'this' for chaining
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;
}
// Build method creates the final immutable object
public HttpRequest build() {
return new HttpRequest(this);
}
}
@Override
public String toString() {
return method + " " + url + "\n"
+ "Headers: " + headers + "\n"
+ "Body: " + (body.isEmpty() ? "(empty)" : body) + "\n"
+ "Timeout: " + timeoutMs + "ms";
}
}
// Usage: clean, readable, no parameter order confusion
HttpRequest request = new HttpRequest.Builder("POST", "https://api.nbi.com/transfer")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body("{\"from\":\"SAV2001\",\"to\":\"CUR3001\",\"amount\":50000}")
.timeout(10000)
.build();
GOF Builder vs Fluent Builder
| Aspect | GOF Builder (Classic) | Fluent Builder (Modern) |
|---|---|---|
| Director | Separate class controls build order | No director, client chains methods |
| Builder location | Separate class/interface | Static inner class |
| Product mutability | Mutable (setters) | Immutable (private constructor) |
| Method chaining | No | Yes (return this) |
| Required vs optional | All steps called by director | Required in constructor, optional via methods |
| Use case | Multiple representations of same construction process | Objects with many optional parameters |
| JDK examples | DocumentBuilder | StringBuilder, Stream.Builder, Locale.Builder |
6. Pre-defined Builder Examples in Java
| Class | Builder Type | Usage |
|---|---|---|
StringBuilder | Fluent | new StringBuilder().append("hello").append(" ").append("world").toString() |
StringBuffer | Fluent | Thread-safe version of StringBuilder |
Stream.Builder | Fluent | Stream.builder().add(1).add(2).add(3).build() |
Locale.Builder | Fluent | new Locale.Builder().setLanguage("en").setRegion("US").build() |
Calendar.Builder | Fluent | new Calendar.Builder().setDate(2026,10,2).build() |
DocumentBuilderFactory | GOF | Creates DocumentBuilder to parse XML |
ProcessBuilder | Fluent | new ProcessBuilder("ls", "-la").directory(dir).start() |
7. Full-Length Industry Project: HTTP Client Request Builder
National Bank of India's backend services communicate with external APIs (payment gateways, credit bureaus, government services). Each HTTP request needs a method, URL, headers, query parameters, body, timeout, retry configuration, and authentication. Some requests need all fields, others need only a few. The team wants a clean, immutable, type-safe way to construct HTTP requests without a 15-parameter constructor.
High-Level Design (HLD)
Low-Level Design (LLD)
Project Structure
HttpRequest.java (Product + Fluent Builder)
package com.nbi.http;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final Map<String, String> queryParams;
private final String body;
private final int timeoutMs;
private final int maxRetries;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers));
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams));
this.body = builder.body;
this.timeoutMs = builder.timeoutMs;
this.maxRetries = builder.maxRetries;
}
public String getMethod() { return method; }
public String getUrl() { return url; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getQueryParams() { return queryParams; }
public String getBody() { return body; }
public int getTimeoutMs() { return timeoutMs; }
public int getMaxRetries() { return maxRetries; }
public String getFullUrl() {
if (queryParams.isEmpty()) return url;
String params = queryParams.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
return url + "?" + params;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(method).append(" ").append(getFullUrl()).append("\n");
headers.forEach((k, v) -> sb.append(" ").append(k).append(": ").append(v).append("\n"));
if (!body.isEmpty()) {
sb.append(" Body: ").append(body.length() > 80 ? body.substring(0, 80) + "..." : body).append("\n");
}
sb.append(" Timeout: ").append(timeoutMs).append("ms | Retries: ").append(maxRetries);
return sb.toString();
}
public static class Builder {
private final String method;
private final String url;
private Map<String, String> headers = new HashMap<>();
private Map<String, String> queryParams = new HashMap<>();
private String body = "";
private int timeoutMs = 5000;
private int maxRetries = 3;
public Builder(String method, String url) {
if (method == null || method.isEmpty()) {
throw new IllegalArgumentException("HTTP method is required");
}
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("URL is required");
}
this.method = method.toUpperCase();
this.url = url;
}
public Builder header(String key, String value) {
this.headers.put(key, value);
return this;
}
public Builder queryParam(String key, String value) {
this.queryParams.put(key, value);
return this;
}
public Builder body(String body) {
this.body = body != null ? body : "";
return this;
}
public Builder timeout(int timeoutMs) {
if (timeoutMs <= 0) {
throw new IllegalArgumentException("Timeout must be positive");
}
this.timeoutMs = timeoutMs;
return this;
}
public Builder retries(int maxRetries) {
if (maxRetries < 0) {
throw new IllegalArgumentException("Retries cannot be negative");
}
this.maxRetries = maxRetries;
return this;
}
// Convenience methods for common patterns
public Builder bearerToken(String token) {
return header("Authorization", "Bearer " + token);
}
public Builder contentTypeJson() {
return header("Content-Type", "application/json");
}
public Builder contentTypeForm() {
return header("Content-Type", "application/x-www-form-urlencoded");
}
public HttpRequest build() {
return new HttpRequest(this);
}
}
}
HttpResponse.java
package com.nbi.http;
import java.util.Map;
public class HttpResponse {
private final int statusCode;
private final String body;
private final Map<String, String> headers;
public HttpResponse(int statusCode, String body, Map<String, String> headers) {
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
}
public int getStatusCode() { return statusCode; }
public String getBody() { return body; }
public Map<String, String> getHeaders() { return headers; }
public boolean isSuccess() { return statusCode >= 200 && statusCode < 300; }
@Override
public String toString() {
return "HTTP " + statusCode + (isSuccess() ? " OK" : " ERROR")
+ " | Body: " + (body.length() > 80 ? body.substring(0, 80) + "..." : body);
}
}
HttpClient.java
package com.nbi.http;
import java.util.Map;
public class HttpClient {
public HttpResponse send(HttpRequest request) {
System.out.println("--- Sending Request ---");
System.out.println(request);
System.out.println();
// Simulate network call based on URL
int statusCode = 200;
String responseBody;
if (request.getUrl().contains("/transfer")) {
responseBody = "{\"status\":\"success\",\"txnId\":\"NEFT123456\"}";
} else if (request.getUrl().contains("/credit-score")) {
responseBody = "{\"score\":780,\"rating\":\"excellent\"}";
} else if (request.getUrl().contains("/balance")) {
responseBody = "{\"accountNo\":\"SAV2001\",\"balance\":250000.00}";
} else if (request.getUrl().contains("/error")) {
statusCode = 500;
responseBody = "{\"error\":\"Internal Server Error\"}";
} else {
responseBody = "{\"message\":\"OK\"}";
}
HttpResponse response = new HttpResponse(statusCode, responseBody,
Map.of("Content-Type", "application/json"));
System.out.println("--- Response ---");
System.out.println(response);
System.out.println();
return response;
}
}
Application.java
package com.nbi.http;
public class Application {
public static void main(String[] args) {
HttpClient client = new HttpClient();
// Scenario 1: Simple GET request (minimal builder usage)
System.out.println("=== Scenario 1: Simple GET ===");
HttpRequest getBalance = new HttpRequest.Builder("GET", "https://api.nbi.com/balance")
.queryParam("accountNo", "SAV2001")
.bearerToken("jwt_token_abc123")
.build();
client.send(getBalance);
// Scenario 2: POST with JSON body (full builder usage)
System.out.println("=== Scenario 2: POST Transfer ===");
HttpRequest transfer = new HttpRequest.Builder("POST", "https://api.nbi.com/transfer")
.contentTypeJson()
.bearerToken("jwt_token_abc123")
.body("{\"from\":\"SAV2001\",\"to\":\"CUR3001\",\"amount\":50000}")
.timeout(10000)
.retries(5)
.build();
client.send(transfer);
// Scenario 3: GET with multiple query params
System.out.println("=== Scenario 3: Credit Score Check ===");
HttpRequest creditCheck = new HttpRequest.Builder("GET", "https://api.creditbureau.com/credit-score")
.queryParam("pan", "ABCDE1234F")
.queryParam("consent", "true")
.header("X-API-Key", "bureau_key_xyz")
.timeout(15000)
.build();
client.send(creditCheck);
// Scenario 4: Immutability verification
System.out.println("=== Scenario 4: Immutability Check ===");
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/test")
.header("Accept", "application/json")
.build();
try {
req.getHeaders().put("Injected", "value");
System.out.println("FAILED: headers are mutable");
} catch (UnsupportedOperationException e) {
System.out.println("PASSED: headers are immutable (UnsupportedOperationException)");
}
// Scenario 5: Error response
System.out.println("\n=== Scenario 5: Error Response ===");
HttpRequest errorReq = new HttpRequest.Builder("POST", "https://api.nbi.com/error")
.contentTypeJson()
.body("{\"bad\":\"data\"}")
.build();
HttpResponse errorResp = client.send(errorReq);
System.out.println("Success? " + errorResp.isSuccess());
// Scenario 6: Validation error
System.out.println("\n=== Scenario 6: Builder Validation ===");
try {
new HttpRequest.Builder("", "https://api.nbi.com/test").build();
} catch (IllegalArgumentException e) {
System.out.println("PASSED: " + e.getMessage());
}
try {
new HttpRequest.Builder("GET", "https://api.nbi.com/test")
.timeout(-1)
.build();
} catch (IllegalArgumentException e) {
System.out.println("PASSED: " + e.getMessage());
}
}
}
Expected Output
HttpRequestBuilderTest.java
package com.nbi.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HttpRequestBuilderTest {
@Test
void build_createsRequestWithRequiredFields() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/test").build();
assertEquals("GET", req.getMethod());
assertEquals("https://api.nbi.com/test", req.getUrl());
}
@Test
void build_methodIsUppercased() {
HttpRequest req = new HttpRequest.Builder("post", "https://api.nbi.com").build();
assertEquals("POST", req.getMethod());
}
@Test
void build_setsDefaultTimeout() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com").build();
assertEquals(5000, req.getTimeoutMs());
}
@Test
void build_setsDefaultRetries() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com").build();
assertEquals(3, req.getMaxRetries());
}
@Test
void build_overridesDefaults() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.timeout(10000)
.retries(5)
.build();
assertEquals(10000, req.getTimeoutMs());
assertEquals(5, req.getMaxRetries());
}
@Test
void build_addsHeaders() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.header("Accept", "application/json")
.header("X-Custom", "value")
.build();
assertEquals("application/json", req.getHeaders().get("Accept"));
assertEquals("value", req.getHeaders().get("X-Custom"));
}
@Test
void build_headersAreImmutable() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.header("Accept", "application/json")
.build();
assertThrows(UnsupportedOperationException.class,
() -> req.getHeaders().put("Injected", "value"));
}
@Test
void build_bearerTokenSetsAuthHeader() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com")
.bearerToken("mytoken")
.build();
assertEquals("Bearer mytoken", req.getHeaders().get("Authorization"));
}
@Test
void build_contentTypeJsonSetsHeader() {
HttpRequest req = new HttpRequest.Builder("POST", "https://api.nbi.com")
.contentTypeJson()
.build();
assertEquals("application/json", req.getHeaders().get("Content-Type"));
}
@Test
void build_queryParamsInFullUrl() {
HttpRequest req = new HttpRequest.Builder("GET", "https://api.nbi.com/search")
.queryParam("q", "hashmap")
.queryParam("limit", "10")
.build();
assertTrue(req.getFullUrl().contains("q=hashmap"));
assertTrue(req.getFullUrl().contains("limit=10"));
}
@Test
void builder_throwsForNullMethod() {
assertThrows(IllegalArgumentException.class,
() -> new HttpRequest.Builder(null, "https://api.nbi.com"));
}
@Test
void builder_throwsForNegativeTimeout() {
assertThrows(IllegalArgumentException.class,
() -> new HttpRequest.Builder("GET", "https://api.nbi.com").timeout(-1));
}
}
Key observations from the project
- Immutable product:
HttpRequesthas onlyfinalfields and a private constructor. Once built, it cannot be modified. Headers and query params are wrapped inCollections.unmodifiableMap. - Required vs optional fields:
methodandurlare required (in the Builder constructor). Everything else is optional with sensible defaults (timeout=5000, retries=3). - Method chaining: Each builder method returns
this, enabling fluent construction:.header(...).body(...).timeout(...).build(). - Convenience methods:
bearerToken(),contentTypeJson(),contentTypeForm()wrap common patterns so the client does not need to know the exact header names. - Validation in Builder: Null/empty method and URL are caught in the Builder constructor. Negative timeout is caught in
timeout(). Errors are thrown beforebuild()creates the object. - Six scenarios tested: Simple GET, POST with body, query params, immutability, error response, validation error.
8. When to Use and When NOT to Use
When to use Builder
| Scenario | Example | Why Builder |
|---|---|---|
| Many constructor parameters | HTTP request, database query, report config | Avoids telescoping constructor. Each parameter is named and optional. |
| Immutable objects | Value objects, configuration, DTOs | Builder collects data, then creates a frozen immutable object. |
| Multiple representations | ConcreteHouse, WoodenHouse, IglooHouse | Same construction process, different product types. |
| Complex object with dependencies | Computer (CPU + RAM + GPU + Storage) | Builder ensures all parts are assembled correctly before use. |
| Step-by-step construction | Document builder, meal builder | Each step adds one part. The order can be fixed (GOF) or flexible (fluent). |
When NOT to use Builder
| Scenario | Why NOT | Use Instead |
|---|---|---|
| Simple objects (1-3 fields) | Builder adds unnecessary code for trivial objects | Constructor or factory method |
| Mutable objects | If the object changes after creation, builder's immutability benefit is lost | Constructor + setters |
| Objects created once at startup | Builder's readability benefit is wasted on a single call | Constructor or config file |
9. Quick Reference Card
| Component | GOF Builder | Fluent Builder |
|---|---|---|
| Product | House (mutable, setters) | HttpRequest (immutable, final fields) |
| Builder | HouseBuilder interface, separate concrete builders | Static inner Builder class |
| Director | CivilEngineer (controls build order) | Not needed (client chains methods) |
| Creation | engineer.constructHouse() | new Builder(...).method().build() |
| Immutable? | No (product has setters) | Yes (private constructor, final fields) |
| JDK example | DocumentBuilderFactory | StringBuilder, Locale.Builder |