Design Patterns

80. Builder Pattern

The Builder pattern solves the telescoping-constructor problem by providing a fluent API to assemble complex objects step-by-step; in Java 16+, records and their wither pattern reduce the need for Builders on simple immutable types.

Java 21 (core language)

Explanation

When a class has many optional parameters, the telescoping constructor anti-pattern (one constructor per combination) becomes unmanageable. The Builder pattern: a static nested Builder class collects parameters via setter-style methods (returning this for fluency), then creates the target object in a final build() method.

Benefits: client code reads like a DSL; parameters can be validated in build(); the target object can be immutable (all fields final); optional parameters have defaults. Drawbacks: verbose โ€” requires a parallel class with all the same fields.

Modern alternative: for simple immutable data, a record with a wither pattern or a compact constructor covers most cases without a separate builder class.

Example

public class BuilderPattern {
    // Immutable class with Builder
    static final class HttpRequest {
        private final String method;
        private final String url;
        private final java.util.Map<String, String> headers;
        private final String body;
        private final int timeoutMs;

        private HttpRequest(Builder b) {
            this.method    = b.method;
            this.url       = b.url;
            this.headers   = java.util.Collections.unmodifiableMap(b.headers);
            this.body      = b.body;
            this.timeoutMs = b.timeoutMs;
        }

        @Override public String toString() {
            return method + " " + url + " (timeout=" + timeoutMs + "ms, headers=" + headers + ")";
        }

        static class Builder {
            // Required
            private final String method;
            private final String url;
            // Optional with defaults
            private java.util.Map<String, String> headers = new java.util.HashMap<>();
            private String body      = null;
            private int    timeoutMs = 5000;

            Builder(String method, String url) {
                this.method = method;
                this.url    = url;
            }

            Builder header(String name, String value) { headers.put(name, value); return this; }
            Builder body(String body)                 { this.body = body;         return this; }
            Builder timeoutMs(int ms)                 { this.timeoutMs = ms;      return this; }

            HttpRequest build() {
                if (method == null || url == null)
                    throw new IllegalStateException("method and url are required");
                return new HttpRequest(this);
            }
        }
    }

    public static void main(String[] args) {
        // Fluent builder
        HttpRequest get = new HttpRequest.Builder("GET", "https://api.example.com/users")
            .header("Authorization", "Bearer token123")
            .header("Accept", "application/json")
            .timeoutMs(10_000)
            .build();
        System.out.println(get);

        HttpRequest post = new HttpRequest.Builder("POST", "https://api.example.com/users")
            .header("Content-Type", "application/json")
            .body("{\"name\": \"Alice\"}")
            .build();
        System.out.println(post);

        // Record wither pattern โ€” simpler alternative for records
        record Point(int x, int y) {
            Point withX(int newX) { return new Point(newX, y); }
            Point withY(int newY) { return new Point(x, newY); }
        }
        var p = new Point(1, 2).withX(5).withY(10);
        System.out.println(p); // Point[x=5, y=10]
    }
}

Sources