Many companies enforce immutable domain objects โ€” objects that can only be set via constructor and expose only getters. The intent is good: thread safety, predictability, no accidental mutation. But as objects grow, the constructor approach breaks down fast.

The Problem with Constructors Alone

Imagine a Client object with 10 fields. When you need to create a client with only 4 of those fields set, you either:

// UNHEALTHY
new Client(1L, "Felix", null,
    null, null, true,
    null, null, null);
// CLEAN
new ClientBuilder()
    .withId(1L)
    .withName("Felix")
    .withPermanent(true)
    .build();

The Solution: Immutable Object + Mutable Builder

The pattern uses two complementary classes: an immutable domain object and a highly mutable builder. The builder accumulates state via fluent setters, then constructs the final immutable object in one build() call.

Client.java โ€” the immutable object

public final class Client {
    private final Long id;
    private final String name;
    private final boolean permanent;

    Client(Long id, String name, boolean permanent) {
        this.id = id;
        this.name = name;
        this.permanent = permanent;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public boolean isPermanent() { return permanent; }
}

ClientBuilder.java โ€” the mutable builder

public class ClientBuilder {
    private Long id;
    private String name;
    private boolean permanent;

    public ClientBuilder withId(Long id) {
        this.id = id;
        return this;
    }

    public ClientBuilder withName(String name) {
        this.name = name;
        return this;
    }

    public ClientBuilder withPermanent(boolean permanent) {
        this.permanent = permanent;
        return this;
    }

    // Copy constructor โ€” create a modified version of an existing Client
    public ClientBuilder from(Client client) {
        this.id = client.getId();
        this.name = client.getName();
        this.permanent = client.isPermanent();
        return this;
    }

    public Client build() {
        Objects.requireNonNull(id, "id is required");
        Objects.requireNonNull(name, "name is required");
        return new Client(id, name, permanent);
    }
}

Three Usage Scenarios

1. Build from scratch

Client client = new ClientBuilder()
    .withId(1L)
    .withName("Felix")
    .withPermanent(true)
    .build();

2. Copy and modify an existing object

// Create a non-permanent copy of an existing client
Client updated = new ClientBuilder()
    .from(existingClient)
    .withPermanent(false)
    .build();

3. The anti-pattern (avoid)

// DON'T DO THIS โ€” package-private constructor, null chaos
Client bad = new Client(1L, "Felix", true); // fine now
Client worse = new Client(null, null, false, null, null, null, null); // nightmare

Why This Pays Off at Scale

When you need to add a new field to Client, you update the builder once and all existing call sites continue to work โ€” they just don't set the new field. With constructor-based construction, every single call site needs updating. On a large codebase this difference is worth days of refactoring.

Joshua Bloch covers this in Effective Java (Item 2) โ€” if you haven't read it, add it to your list. It's still the best Java book written.