When is Builder the right choice over a telescoping const… — Cracked Java
// Object-Oriented Programming · Creational Design Patterns
MidTheoryCodingAmazon

When is Builder the right choice over a telescoping constructor?

Builder belongs when a constructor would have four-or-more parameters, several of them optional, and you want the resulting object to be immutable. Telescoping constructors (one for each combination of optional params) explode combinatorially and silently mis-order positional arguments. JavaBeans-style setters compromise immutability and let you observe a partially-initialized object. The Builder gives you a fluent, readable, immutable result.

The pain — telescoping constructors

public class NutritionFacts {
    public NutritionFacts(int servingSize, int servings) { ... }
    public NutritionFacts(int servingSize, int servings, int calories) { ... }
    public NutritionFacts(int servingSize, int servings, int calories, int fat) { ... }
    public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { ... }
    // ... 5 more
}

new NutritionFacts(240, 8, 100, 0, 35, 27);   // what does '35' mean?

Six positional ints. The compiler can't catch a swap of sodium and carbohydrate if both are int. Adding a new optional field requires another constructor.

The pain — JavaBeans pattern

NutritionFacts n = new NutritionFacts();
n.setServingSize(240);
n.setServings(8);
n.setCalories(100);
// ... 7 more setters

Readable, but:

  • The object exists in invalid states between new and the last setter.
  • Cross-field validation can't run until "the end" — but there's no defined end.
  • The object can't be made immutable; setters preclude final.

The Builder

public final class NutritionFacts {
    private final int servingSize, servings, calories, fat, sodium, carbohydrate;

    private NutritionFacts(Builder b) {
        this.servingSize  = b.servingSize;
        this.servings     = b.servings;
        this.calories     = b.calories;
        this.fat          = b.fat;
        this.sodium       = b.sodium;
        this.carbohydrate = b.carbohydrate;
    }

    public static class Builder {
        // required
        private final int servingSize;
        private final int servings;
        // optional, with defaults
        private int calories     = 0;
        private int fat          = 0;
        private int sodium       = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings    = servings;
        }
        public Builder calories(int v)     { this.calories = v; return this; }
        public Builder fat(int v)          { this.fat = v; return this; }
        public Builder sodium(int v)       { this.sodium = v; return this; }
        public Builder carbohydrate(int v) { this.carbohydrate = v; return this; }

        public NutritionFacts build() {
            if (calories < 0) throw new IllegalArgumentException();
            // ... other cross-field validation
            return new NutritionFacts(this);
        }
    }
}

// Usage
NutritionFacts cola = new NutritionFacts.Builder(240, 8)
    .calories(100)
    .sodium(35)
    .carbohydrate(27)
    .build();

What you get:

  • Required fields are constructor parameters of the builder — the compiler enforces them.
  • Optional fields are named method calls — readable, can't be mis-ordered, defaultable.
  • The product is immutable — every field final, no setters.
  • Cross-field validation runs once, in build(), before the product is exposed.

When NOT to use Builder

  • 2-3 fields, all required. Just use a constructor or a record.
  • Records: record Point(int x, int y) {} — no Builder needed.
  • Mutable objects. If callers will modify the object later, setters are fine.

Modern Java alternatives

  • Records with static factory methods: Point.of(1, 2). Records make Builder unnecessary for the simple immutable-data case.
  • Records + canonical constructor validation: catches invariant violations at construction.
  • Sealed records + factory methods: typed alternatives.

But for objects with 4+ parameters and a mix of required/optional, Builder still earns its keep.

A JDK example

java.net.http.HttpRequest:

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com"))
    .GET()
    .timeout(Duration.ofSeconds(10))
    .header("Accept", "application/json")
    .build();

StringBuilder is not a GoF Builder — it's a mutable string buffer. Easy confusion in interviews.

Mark your status