Mediumcreational18 min read

Builder Design Pattern

1. What is Builder Pattern?

Definition

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

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

Solution

The Builder pattern separates object construction from its representation using four components:

ComponentRoleHouse Example
a) ProductThe complex object being builtHouse
b) Builder (interface)Declares the steps needed to build the productHouseBuilder
c) ConcreteBuilderImplements the builder for a specific product typeIglooHouseBuilder, ConcreteHouseBuilder, WoodenHouseBuilder
d) DirectorUses the builder to construct the product in a specific order. Like a CivilEngineer who uses the Builder support to create the product.CivilEngineer
Director (CivilEngineer) | +-- uses --> HouseBuilder (interface) | +------------+-------------+------------------+ | | | | IglooHouse WoodenHouse ConcreteHouse MarbleHouse Builder Builder Builder Builder Each builder creates: House (Product) +-- Basement (IceBasement / WoodenBasement / ConcreteBasement) +-- Structure (IceStructure / WoodenStructure / ConcreteStructure) +-- Roofing (IceRoofing / WoodenRoofing / ConcreteRoofing) +-- Interior (IceCurvingInterior / WoodenInterior / ConcreteInterior)

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);
    }
}
=== Concrete House === House [ Basement = Concrete Basement Structure = Concrete Pillars Structure Roofing = Concrete Slab Roofing Interior = Concrete Interior with Paint ] === Wooden House === House [ Basement = Wooden Basement Structure = Wooden Pillars Structure Roofing = Wooden Roofing Interior = Wooden Panel Interior ] === Igloo House === House [ Basement = Ice Basement Structure = Ice Blocks Structure Roofing = Ice Dome Roofing Interior = Ice Curving Interior ]

5. Modern Fluent Builder (Joshua Bloch Style)

Modern Approach

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

AspectGOF Builder (Classic)Fluent Builder (Modern)
DirectorSeparate class controls build orderNo director, client chains methods
Builder locationSeparate class/interfaceStatic inner class
Product mutabilityMutable (setters)Immutable (private constructor)
Method chainingNoYes (return this)
Required vs optionalAll steps called by directorRequired in constructor, optional via methods
Use caseMultiple representations of same construction processObjects with many optional parameters
JDK examplesDocumentBuilderStringBuilder, Stream.Builder, Locale.Builder

6. Pre-defined Builder Examples in Java

ClassBuilder TypeUsage
StringBuilderFluentnew StringBuilder().append("hello").append(" ").append("world").toString()
StringBufferFluentThread-safe version of StringBuilder
Stream.BuilderFluentStream.builder().add(1).add(2).add(3).build()
Locale.BuilderFluentnew Locale.Builder().setLanguage("en").setRegion("US").build()
Calendar.BuilderFluentnew Calendar.Builder().setDate(2026,10,2).build()
DocumentBuilderFactoryGOFCreates DocumentBuilder to parse XML
ProcessBuilderFluentnew ProcessBuilder("ls", "-la").directory(dir).start()

7. Full-Length Industry Project: HTTP Client Request Builder

Business Scenario

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)

+---------------------------------------------------+ | SERVICE LAYER | | PaymentService CreditCheckService | | | | | | +-- builds request using HttpRequest.Builder | | +-- sends via HttpClient | +----------------------------+----------------------+ | v +---------------------------------------------------+ | BUILDER LAYER | | | | HttpRequest (immutable product) | | +-- Builder (static inner class) | | +-- method(String) [required] | | +-- url(String) [required] | | +-- header(k, v) [optional, chainable]| | +-- queryParam(k, v) [optional, chainable]| | +-- body(String) [optional] | | +-- timeout(int) [optional, default] | | +-- retries(int) [optional, default] | | +-- bearerToken(String)[optional] | | +-- build() [creates product] | +---------------------------------------------------+ | v +---------------------------------------------------+ | HTTP CLIENT LAYER | | HttpClient | | +-- send(HttpRequest): HttpResponse | | +-- logs request details | | +-- simulates network call | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | HttpRequest (immutable product) | +-------------------------------------------------------+ | - method: String [final] | | - url: String [final] | | - headers: Map<String, String> [final, unmodifiable]| | - queryParams: Map<String, String> [final, unmodifiable]| | - body: String [final] | | - timeoutMs: int [final] | | - maxRetries: int [final] | +-------------------------------------------------------+ | - HttpRequest(Builder) [private] | | + getMethod(): String | | + getUrl(): String | | + getFullUrl(): String (url + query params) | | + getHeaders(): Map | | + getBody(): String | | + getTimeoutMs(): int | | + getMaxRetries(): int | +-------------------------------------------------------+ | + static class Builder | | - method: String [required] | | - url: String [required] | | - headers: Map [default empty] | | - queryParams: Map [default empty] | | - body: String [default empty] | | - timeoutMs: int [default 5000] | | - maxRetries: int [default 3] | | + Builder(method, url) [constructor] | | + header(k, v): Builder | | + queryParam(k, v): Builder | | + body(String): Builder | | + timeout(int): Builder | | + retries(int): Builder | | + bearerToken(String): Builder | | + contentTypeJson(): Builder | | + build(): HttpRequest | +-------------------------------------------------------+ +-------------------------------------------------------+ | HttpResponse | +-------------------------------------------------------+ | - statusCode: int | | - body: String | | - headers: Map | | + isSuccess(): boolean | +-------------------------------------------------------+ +-------------------------------------------------------+ | HttpClient | +-------------------------------------------------------+ | + send(request): HttpResponse | | - logRequest(request): void | +-------------------------------------------------------+

Project Structure

nbi-http-client/ ├── src/ │ └── main/java/com/nbi/http/ │ ├── HttpRequest.java (immutable product + fluent builder) │ ├── HttpResponse.java │ ├── HttpClient.java (sends requests) │ └── Application.java ├── src/ │ └── test/java/com/nbi/http/ │ ├── HttpRequestBuilderTest.java │ └── HttpClientTest.java └── pom.xml

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

=== Scenario 1: Simple GET === --- Sending Request --- GET https://api.nbi.com/balance?accountNo=SAV2001 Authorization: Bearer jwt_token_abc123 Timeout: 5000ms | Retries: 3 --- Response --- HTTP 200 OK | Body: {"accountNo":"SAV2001","balance":250000.00} === Scenario 2: POST Transfer === --- Sending Request --- POST https://api.nbi.com/transfer Content-Type: application/json Authorization: Bearer jwt_token_abc123 Body: {"from":"SAV2001","to":"CUR3001","amount":50000} Timeout: 10000ms | Retries: 5 --- Response --- HTTP 200 OK | Body: {"status":"success","txnId":"NEFT123456"} === Scenario 3: Credit Score Check === --- Sending Request --- GET https://api.creditbureau.com/credit-score?pan=ABCDE1234F&consent=true X-API-Key: bureau_key_xyz Timeout: 15000ms | Retries: 3 --- Response --- HTTP 200 OK | Body: {"score":780,"rating":"excellent"} === Scenario 4: Immutability Check === PASSED: headers are immutable (UnsupportedOperationException) === Scenario 5: Error Response === --- Sending Request --- POST https://api.nbi.com/error Content-Type: application/json Body: {"bad":"data"} Timeout: 5000ms | Retries: 3 --- Response --- HTTP 500 ERROR | Body: {"error":"Internal Server Error"} Success? false === Scenario 6: Builder Validation === PASSED: HTTP method is required PASSED: Timeout must be positive

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

  1. Immutable product: HttpRequest has only final fields and a private constructor. Once built, it cannot be modified. Headers and query params are wrapped in Collections.unmodifiableMap.
  2. Required vs optional fields: method and url are required (in the Builder constructor). Everything else is optional with sensible defaults (timeout=5000, retries=3).
  3. Method chaining: Each builder method returns this, enabling fluent construction: .header(...).body(...).timeout(...).build().
  4. Convenience methods: bearerToken(), contentTypeJson(), contentTypeForm() wrap common patterns so the client does not need to know the exact header names.
  5. Validation in Builder: Null/empty method and URL are caught in the Builder constructor. Negative timeout is caught in timeout(). Errors are thrown before build() creates the object.
  6. 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

ScenarioExampleWhy Builder
Many constructor parametersHTTP request, database query, report configAvoids telescoping constructor. Each parameter is named and optional.
Immutable objectsValue objects, configuration, DTOsBuilder collects data, then creates a frozen immutable object.
Multiple representationsConcreteHouse, WoodenHouse, IglooHouseSame construction process, different product types.
Complex object with dependenciesComputer (CPU + RAM + GPU + Storage)Builder ensures all parts are assembled correctly before use.
Step-by-step constructionDocument builder, meal builderEach step adds one part. The order can be fixed (GOF) or flexible (fluent).

When NOT to use Builder

ScenarioWhy NOTUse Instead
Simple objects (1-3 fields)Builder adds unnecessary code for trivial objectsConstructor or factory method
Mutable objectsIf the object changes after creation, builder's immutability benefit is lostConstructor + setters
Objects created once at startupBuilder's readability benefit is wasted on a single callConstructor or config file

9. Quick Reference Card

ComponentGOF BuilderFluent Builder
ProductHouse (mutable, setters)HttpRequest (immutable, final fields)
BuilderHouseBuilder interface, separate concrete buildersStatic inner Builder class
DirectorCivilEngineer (controls build order)Not needed (client chains methods)
Creationengineer.constructHouse()new Builder(...).method().build()
Immutable?No (product has setters)Yes (private constructor, final fields)
JDK exampleDocumentBuilderFactoryStringBuilder, Locale.Builder
Creational GOF Pattern Medium

Builder Design Pattern in Java

Complete Tutorial | 11 Sections | Classic GOF Builder + Modern Fluent Builder | Full HTTP Request Builder Project with HLD and LLD | House Construction Example | Multiple Scenarios | 12 Interview Questions

1. What is Builder Pattern?

Definition

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

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

Solution

The Builder pattern separates object construction from its representation using four components:

ComponentRoleHouse Example
a) ProductThe complex object being builtHouse
b) Builder (interface)Declares the steps needed to build the productHouseBuilder
c) ConcreteBuilderImplements the builder for a specific product typeIglooHouseBuilder, ConcreteHouseBuilder, WoodenHouseBuilder
d) DirectorUses the builder to construct the product in a specific order. Like a CivilEngineer who uses the Builder support to create the product.CivilEngineer
Director (CivilEngineer) | +-- uses --> HouseBuilder (interface) | +------------+-------------+------------------+ | | | | IglooHouse WoodenHouse ConcreteHouse MarbleHouse Builder Builder Builder Builder Each builder creates: House (Product) +-- Basement (IceBasement / WoodenBasement / ConcreteBasement) +-- Structure (IceStructure / WoodenStructure / ConcreteStructure) +-- Roofing (IceRoofing / WoodenRoofing / ConcreteRoofing) +-- Interior (IceCurvingInterior / WoodenInterior / ConcreteInterior)

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);
    }
}
=== Concrete House === House [ Basement = Concrete Basement Structure = Concrete Pillars Structure Roofing = Concrete Slab Roofing Interior = Concrete Interior with Paint ] === Wooden House === House [ Basement = Wooden Basement Structure = Wooden Pillars Structure Roofing = Wooden Roofing Interior = Wooden Panel Interior ] === Igloo House === House [ Basement = Ice Basement Structure = Ice Blocks Structure Roofing = Ice Dome Roofing Interior = Ice Curving Interior ]

5. Modern Fluent Builder (Joshua Bloch Style)

Modern Approach

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

AspectGOF Builder (Classic)Fluent Builder (Modern)
DirectorSeparate class controls build orderNo director, client chains methods
Builder locationSeparate class/interfaceStatic inner class
Product mutabilityMutable (setters)Immutable (private constructor)
Method chainingNoYes (return this)
Required vs optionalAll steps called by directorRequired in constructor, optional via methods
Use caseMultiple representations of same construction processObjects with many optional parameters
JDK examplesDocumentBuilderStringBuilder, Stream.Builder, Locale.Builder

6. Pre-defined Builder Examples in Java

ClassBuilder TypeUsage
StringBuilderFluentnew StringBuilder().append("hello").append(" ").append("world").toString()
StringBufferFluentThread-safe version of StringBuilder
Stream.BuilderFluentStream.builder().add(1).add(2).add(3).build()
Locale.BuilderFluentnew Locale.Builder().setLanguage("en").setRegion("US").build()
Calendar.BuilderFluentnew Calendar.Builder().setDate(2026,10,2).build()
DocumentBuilderFactoryGOFCreates DocumentBuilder to parse XML
ProcessBuilderFluentnew ProcessBuilder("ls", "-la").directory(dir).start()

7. Full-Length Industry Project: HTTP Client Request Builder

Business Scenario

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)

+---------------------------------------------------+ | SERVICE LAYER | | PaymentService CreditCheckService | | | | | | +-- builds request using HttpRequest.Builder | | +-- sends via HttpClient | +----------------------------+----------------------+ | v +---------------------------------------------------+ | BUILDER LAYER | | | | HttpRequest (immutable product) | | +-- Builder (static inner class) | | +-- method(String) [required] | | +-- url(String) [required] | | +-- header(k, v) [optional, chainable]| | +-- queryParam(k, v) [optional, chainable]| | +-- body(String) [optional] | | +-- timeout(int) [optional, default] | | +-- retries(int) [optional, default] | | +-- bearerToken(String)[optional] | | +-- build() [creates product] | +---------------------------------------------------+ | v +---------------------------------------------------+ | HTTP CLIENT LAYER | | HttpClient | | +-- send(HttpRequest): HttpResponse | | +-- logs request details | | +-- simulates network call | +---------------------------------------------------+

Low-Level Design (LLD)

+-------------------------------------------------------+ | HttpRequest (immutable product) | +-------------------------------------------------------+ | - method: String [final] | | - url: String [final] | | - headers: Map<String, String> [final, unmodifiable]| | - queryParams: Map<String, String> [final, unmodifiable]| | - body: String [final] | | - timeoutMs: int [final] | | - maxRetries: int [final] | +-------------------------------------------------------+ | - HttpRequest(Builder) [private] | | + getMethod(): String | | + getUrl(): String | | + getFullUrl(): String (url + query params) | | + getHeaders(): Map | | + getBody(): String | | + getTimeoutMs(): int | | + getMaxRetries(): int | +-------------------------------------------------------+ | + static class Builder | | - method: String [required] | | - url: String [required] | | - headers: Map [default empty] | | - queryParams: Map [default empty] | | - body: String [default empty] | | - timeoutMs: int [default 5000] | | - maxRetries: int [default 3] | | + Builder(method, url) [constructor] | | + header(k, v): Builder | | + queryParam(k, v): Builder | | + body(String): Builder | | + timeout(int): Builder | | + retries(int): Builder | | + bearerToken(String): Builder | | + contentTypeJson(): Builder | | + build(): HttpRequest | +-------------------------------------------------------+ +-------------------------------------------------------+ | HttpResponse | +-------------------------------------------------------+ | - statusCode: int | | - body: String | | - headers: Map | | + isSuccess(): boolean | +-------------------------------------------------------+ +-------------------------------------------------------+ | HttpClient | +-------------------------------------------------------+ | + send(request): HttpResponse | | - logRequest(request): void | +-------------------------------------------------------+

Project Structure

nbi-http-client/ ├── src/ │ └── main/java/com/nbi/http/ │ ├── HttpRequest.java (immutable product + fluent builder) │ ├── HttpResponse.java │ ├── HttpClient.java (sends requests) │ └── Application.java ├── src/ │ └── test/java/com/nbi/http/ │ ├── HttpRequestBuilderTest.java │ └── HttpClientTest.java └── pom.xml

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

=== Scenario 1: Simple GET === --- Sending Request --- GET https://api.nbi.com/balance?accountNo=SAV2001 Authorization: Bearer jwt_token_abc123 Timeout: 5000ms | Retries: 3 --- Response --- HTTP 200 OK | Body: {"accountNo":"SAV2001","balance":250000.00} === Scenario 2: POST Transfer === --- Sending Request --- POST https://api.nbi.com/transfer Content-Type: application/json Authorization: Bearer jwt_token_abc123 Body: {"from":"SAV2001","to":"CUR3001","amount":50000} Timeout: 10000ms | Retries: 5 --- Response --- HTTP 200 OK | Body: {"status":"success","txnId":"NEFT123456"} === Scenario 3: Credit Score Check === --- Sending Request --- GET https://api.creditbureau.com/credit-score?pan=ABCDE1234F&consent=true X-API-Key: bureau_key_xyz Timeout: 15000ms | Retries: 3 --- Response --- HTTP 200 OK | Body: {"score":780,"rating":"excellent"} === Scenario 4: Immutability Check === PASSED: headers are immutable (UnsupportedOperationException) === Scenario 5: Error Response === --- Sending Request --- POST https://api.nbi.com/error Content-Type: application/json Body: {"bad":"data"} Timeout: 5000ms | Retries: 3 --- Response --- HTTP 500 ERROR | Body: {"error":"Internal Server Error"} Success? false === Scenario 6: Builder Validation === PASSED: HTTP method is required PASSED: Timeout must be positive

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

  1. Immutable product: HttpRequest has only final fields and a private constructor. Once built, it cannot be modified. Headers and query params are wrapped in Collections.unmodifiableMap.
  2. Required vs optional fields: method and url are required (in the Builder constructor). Everything else is optional with sensible defaults (timeout=5000, retries=3).
  3. Method chaining: Each builder method returns this, enabling fluent construction: .header(...).body(...).timeout(...).build().
  4. Convenience methods: bearerToken(), contentTypeJson(), contentTypeForm() wrap common patterns so the client does not need to know the exact header names.
  5. Validation in Builder: Null/empty method and URL are caught in the Builder constructor. Negative timeout is caught in timeout(). Errors are thrown before build() creates the object.
  6. 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

ScenarioExampleWhy Builder
Many constructor parametersHTTP request, database query, report configAvoids telescoping constructor. Each parameter is named and optional.
Immutable objectsValue objects, configuration, DTOsBuilder collects data, then creates a frozen immutable object.
Multiple representationsConcreteHouse, WoodenHouse, IglooHouseSame construction process, different product types.
Complex object with dependenciesComputer (CPU + RAM + GPU + Storage)Builder ensures all parts are assembled correctly before use.
Step-by-step constructionDocument builder, meal builderEach step adds one part. The order can be fixed (GOF) or flexible (fluent).

When NOT to use Builder

ScenarioWhy NOTUse Instead
Simple objects (1-3 fields)Builder adds unnecessary code for trivial objectsConstructor or factory method
Mutable objectsIf the object changes after creation, builder's immutability benefit is lostConstructor + setters
Objects created once at startupBuilder's readability benefit is wasted on a single callConstructor or config file

9. Quick Reference Card

ComponentGOF BuilderFluent Builder
ProductHouse (mutable, setters)HttpRequest (immutable, final fields)
BuilderHouseBuilder interface, separate concrete buildersStatic inner Builder class
DirectorCivilEngineer (controls build order)Not needed (client chains methods)
Creationengineer.constructHouse()new Builder(...).method().build()
Immutable?No (product has setters)Yes (private constructor, final fields)
JDK exampleDocumentBuilderFactoryStringBuilder, Locale.Builder