🏛️ Design Principles & Patterns · Advanced

Builder in Java

Readable construction of objects with many optional parameters.

🧩 The mysterynew Pizza(12, true, false, true). Is the second true cheese or olives? Nobody knows, including the person who wrote it last week.

Telescoping trouble

With many optional parameters, you end up with telescoping constructors: one with 2 args, one with 3, one with 4... Call sites become a row of mystery values, and swapping two booleans compiles happily. Readability collapses.

new Pizza(12);
new Pizza(12, true);
new Pizza(12, true, false, true); // ???

The builder shape

A Builder collects values through named methods. Each method stores a value and **returns this**, so calls chain. Fields start with defaults. At the end, build() creates the real object in one go.

int size = 12;      // default
boolean cheese;
PizzaBuilder size(int s) {
    size = s;
    return this;    // enables chaining
}
Pizza build() {
    return new Pizza(size, cheese);
}
🔮 Predict it

Defaults survive

What does this print?

record P(int size, boolean cheese) {}
class B {
    int size = 12; boolean cheese;
    B size(int s) { size = s; return this; }
    B cheese() { cheese = true; return this; }
    P build() { return new P(size, cheese); }
}
void main() {
    IO.println(new B().size(16).build());
}
  1. P[size=16, cheese=false]
  2. P[size=12, cheese=false]
  3. P[16, false]
Show the answer

size(16) replaced the default 12, and cheese() was never called, so the flag kept its default false. A record's toString prints name=value pairs.

⚠️ The trap

Forgetting return this

Each builder method declares the builder as its return type, so it must return this. Forget it and the compiler stops you with "missing return statement". port() is the culprit.

Builder host(String h) {
    host = h; return this;
}
Builder port(int p) { port = p; }

build() is the gatekeeper

build() sees all values at once, so it's the natural place to check that required values were set and that fields agree with each other, like min <= max. Only then is the object created, and it can be immutable: no half-built object ever escapes.

Range build() {
    if (min > max) throw new
        IllegalStateException("min > max");
    return new Range(min, max);
}

Builder vs JavaBeans setters

✗ JavaBeans
var p = new Pizza();
p.setSize(16);
// p is usable here, half-built
p.setCheese(true);

The object exists before it's complete and can't be immutable.

✓ Builder
var p = new PizzaBuilder()
    .size(16)
    .cheese()
    .build();

Reads like named arguments; the object appears only when complete.

🤔 Think first

Every option has a cost

Telescoping constructors, JavaBeans setters, a Map<String, Object> of options, or a Builder: what's the main drawback of each?

Think about it, then reveal the answer

Telescoping: hard to tell which argument is which. JavaBeans: the object may be used half-initialized. Map of options: no compile-time type checking. Builder: an extra class and a bit more code. That small cost is why Effective Java recommends builders for many optional parameters.

💼 In the real world

Builders in the wild

You'll meet builders everywhere: HttpRequest.newBuilder().uri(...).timeout(...).build(), test-data builders, and generated ones via tools like Lombok. With 2 required and 9 optional parameters, a builder is the textbook answer in reviews and interviews.

Key takeaways

  1. Each setter-like method returns this, so calls chain
  2. build() validates once and constructs the final object
  3. The result can be immutable; no half-built object escapes

💡 Ordering a custom sandwich step by step, then the cashier hands you one finished sandwich.

🤯 Did you know?

The original 1994 Gang of Four Builder solved a different problem: one RTF reader that could build different outputs, such as plain ASCII or TeX, through interchangeable converters.

Practice questions

What does this print?

record P(String size, boolean cheese) {}
class B {
    String size = "M"; boolean cheese;
    B size(String s) { size = s; return this; }
    B cheese() { cheese = true; return this; }
    P build() { return new P(size, cheese); }
}
void main() {
    IO.println(new B().cheese().build());
}
  1. P[size=null, cheese=true]
  2. P[size=M, cheese=true]
  3. P[size=M, cheese=false]
  4. P[M, true]
Check your answer

P[size=M, cheese=true]. size() was never called, so the builder's default "M" is used, and cheese() set the flag. Record toString prints name=value pairs.

A class has 2 required and 9 optional parameters. Which construction approach does Effective Java recommend?

  1. Telescoping constructors
  2. A no-arg constructor plus setters (JavaBeans)
  3. A Builder
  4. One constructor taking a Map<String, Object>
Check your answer

A Builder. A builder reads like named arguments, can validate in build(), and produces an immutable object. Telescoping constructors are unreadable and a Map loses type safety.

Next: a GPS app swaps between fastest, shortest and no-tolls without redrawing the map. That's the Strategy pattern.