iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Builder

The Builder pattern constructs complex objects step by step. It shines when a class has many optional fields, when construction has invariants you want to validate at the end, or when you want a fluent, readable API for tests and configuration.

Fluent builder + Director + records

EXAMPLE
// 1) The problem — telescoping constructors
public class Pizza {
    public Pizza(String size) { /* ... */ }
    public Pizza(String size, boolean cheese) { /* ... */ }
    public Pizza(String size, boolean cheese, boolean pepperoni) { /* ... */ }
    public Pizza(String size, boolean cheese, boolean pepperoni, boolean mushrooms, boolean olives) { /* ... */ }
    // … 7 ctor overloads later …
}

new Pizza("large", true, false, true, false);          // which boolean was which?

// 2) Builder solution — fluent, immutable result
public final class Pizza {
    private final String size;
    private final List<String> toppings;
    private final String crust;
    private final boolean extraCheese;

    private Pizza(Builder b) {
        this.size        = b.size;
        this.toppings    = List.copyOf(b.toppings);     // defensive copy
        this.crust       = b.crust;
        this.extraCheese = b.extraCheese;
    }

    public static Builder builder() { return new Builder(); }

    public static final class Builder {
        private String size;
        private List<String> toppings = new ArrayList<>();
        private String crust = "thin";
        private boolean extraCheese;

        public Builder size(String s)                 { this.size = s; return this; }
        public Builder topping(String t)              { toppings.add(t); return this; }
        public Builder toppings(String... t)          { toppings.addAll(List.of(t)); return this; }
        public Builder crust(String c)                { this.crust = c; return this; }
        public Builder extraCheese()                  { this.extraCheese = true; return this; }

        public Pizza build() {
            // Validate INVARIANTS in build(), not in setters
            if (size == null) throw new IllegalStateException("size is required");
            if (toppings.isEmpty()) throw new IllegalStateException("at least one topping");
            return new Pizza(this);
        }
    }
}

// Usage
Pizza p = Pizza.builder()
    .size("large")
    .crust("thick")
    .toppings("pepperoni", "mushrooms")
    .extraCheese()
    .build();

// 3) Required vs optional — split with a sub-builder
public class Email {
    private final String to, subject, body;

    public static Subject builder(String to) { return new Builder(to); }

    public interface Subject { Body subject(String s); }
    public interface Body    { Optional optional(); Email send(); /* etc. */ }

    public static final class Builder implements Subject, Body { /* … */ }
}

// Now the type system FORCES callers to specify required fields in order:
Email.builder("bob@example.com")
    .subject("hello")
    .build();          // won't compile without .body() if step interface required it

// 4) Director — encapsulate a recipe
public class PizzaDirector {
    public static Pizza margherita() {
        return Pizza.builder().size("medium").toppings("mozzarella", "basil", "tomato").build();
    }
    public static Pizza pepperoni() {
        return Pizza.builder().size("large").toppings("pepperoni").extraCheese().build();
    }
}
// Useful when the same complex construction is repeated across the codebase.

// 5) Builder vs constructor with named-args (Kotlin, Scala, Python)
// Kotlin
val p = Pizza(size = "large", crust = "thick", toppings = listOf("pepperoni"))
// Python
p = Pizza(size="large", crust="thick", toppings=["pepperoni"])
//
// When the language has named/default arguments, the builder is often unnecessary boilerplate.
// Reach for the builder when: invariants only valid AFTER multiple steps, or when the API will be
// chained across many call sites in a DSL-like way.

// 6) Builder for HTTP clients (real-world)
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(Duration.ofSeconds(2))
    .readTimeout(Duration.ofSeconds(10))
    .retryOnConnectionFailure(true)
    .addInterceptor(new AuthInterceptor(token))
    .build();

Request req = new Request.Builder()
    .url("https://api.example.com/users/42")
    .header("Accept", "application/json")
    .get()
    .build();

// 7) Builder + Records (Java 16+) — concise immutable result
public record Order(String id, String customerId, long totalCents, List<LineItem> items) {
    public Order {
        Objects.requireNonNull(id);
        items = List.copyOf(items);
    }
    public static Builder builder() { return new Builder(); }

    public static final class Builder {
        private String id;
        private String customerId;
        private long totalCents;
        private List<LineItem> items = new ArrayList<>();
        public Builder id(String v) { this.id = v; return this; }
        public Builder customerId(String v) { this.customerId = v; return this; }
        public Builder totalCents(long v) { this.totalCents = v; return this; }
        public Builder add(LineItem li) { items.add(li); return this; }
        public Order build() { return new Order(id, customerId, totalCents, items); }
    }
}

// 8) Lombok @Builder (Java) — generates the builder for you
@Builder
public class User {
    @NonNull private final String name;
    private final String email;
    @Builder.Default private final boolean active = true;
}

User u = User.builder().name("mara").email("mara@example.com").build();

// 9) Test data builder pattern — clean fixtures
public class OrderTestBuilder {
    private String id = "o-1";
    private String customerId = "c-1";
    private long total = 4999;
    private List<LineItem> items = new ArrayList<>(List.of(LineItemTestBuilder.aLineItem().build()));

    public static OrderTestBuilder anOrder() { return new OrderTestBuilder(); }
    public OrderTestBuilder withTotal(long v) { this.total = v; return this; }
    public OrderTestBuilder withId(String v) { this.id = v; return this; }
    public OrderTestBuilder withNoItems() { this.items.clear(); return this; }
    public Order build() { return new Order(id, customerId, total, items); }
}

// Tests read like prose:
Order o = anOrder().withTotal(0).build();
assertThat(checkoutService.discountFor(o)).isZero();

// 10) Variations
//   Step Builder       — interfaces enforce required-field order at compile time
//   Generic Builder    — base builder<T extends Builder<T>> with self() for chaining in subclasses
//   Mutable Result     — when the built object IS mutable (rare; prefer immutable)
//   Copy-Modify Builder — toBuilder() on the result so callers can derive a variation
//                         (Lombok @Builder(toBuilder = true), Kotlin data class .copy())

// 11) When to use Builder
//   ✓ Many optional fields with invariants
//   ✓ Immutable result you want to construct in stages
//   ✓ Need a fluent DSL feel (HTTP requests, query builders, test fixtures)
//   ✓ Cross-package public API where you can't add named arguments later

// 12) When NOT to use Builder
//   ✗ The language has named + default parameters (Kotlin, Python, Scala) — usually unnecessary
//   ✗ The object has few fields and all are required — a constructor or record is clearer
//   ✗ Construction has no invariants worth deferring to a build() call

// 13) Common bugs
//   • Forgetting build() — caller gets a Builder, not the object
//   • Mutating internal collections after build() — defensive copy in the ctor or build()
//   • Allowing build() without required fields — validate in build(), throw IllegalStateException
//   • Builder shared between threads — builders are per-construction; don't reuse
//   • Builder + Setter on the result — defeats immutability; pick one
//   • Builder method names with 'set' prefix — convention is the noun (.timeout(...) not .setTimeout(...))

Why it matters

Builder shines when the result is immutable, has many optional fields, and needs end-of-construction validation. In languages with named and default arguments (Kotlin, Python, Scala), a regular constructor often beats a builder for clarity — reach for the pattern in Java, C#, and Go where you don’t have that convenience, or any time the fluent style genuinely improves the API.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
class HttpRequestBuilder {
    constructor() { this.req = {}; }
    url(u)     { this.req.url = u; return this; }
    method(m)  { this.req.method = m; return this; }
    header(k, v) { (this.req.headers ??= {})[k] = v; return this; }
    build()    { return this.req; }
}
const req = new HttpRequestBuilder().url('/').method('GET').build();
Try it Yourself »

Discussion

Loading…