Creational · 3 of 5
Builder
Builder is a creational pattern for complex objects: set named optional parts in any order, then call build() once. The builder validates required fields and returns an immutable product.
Without it you get an eight-argument constructor like SearchQuery(q, tags, minYear, maxYear, openAccess, sort, page, size)— three nulls, unclear zeros, and the next requirement adds “peer reviewed.” make that class painful for callers. Builder names each optional part instead.
Many optional pieces, or an honest constructor?
What 0 means
new SearchQuery(q, tags, null, 2020,
false, null, 0, 50)Is 0 “first page” or “unset”? Is null sort the default? Callers guess.
Named, validated
SearchQuery.builder("crispr")
.tag("reviews")
.minYear(2020)
.pageSize(50)
.build()Required text is in the builder constructor. Optional parts are . Illegal combinations die in build().
Construction timeline
Three gates, then an immutable product
- 1
Required
Builder constructor or mandatory setters that build() checks. Text cannot be blank.
- 2
Optional
Named fluent methods. Defaults live here (pageSize = 20). Order should not matter.
- 3
build()
Validate combinations. Copy collections. Return an immutable SearchQuery.
Named parts, one validation gate
final class SearchQuery {
private final String text;
private final List<String> tags;
private final Integer minYear;
private final int pageSize;
private SearchQuery(Builder b) {
this.text = b.text;
this.tags = List.copyOf(b.tags);
this.minYear = b.minYear;
this.pageSize = b.pageSize;
}
static Builder builder(String text) { return new Builder(text); }
static final class Builder {
private final String text;
private final List<String> tags = new ArrayList<>();
private Integer minYear;
private int pageSize = 20;
private Builder(String text) {
this.text = Objects.requireNonNull(text);
}
Builder tag(String tag) { tags.add(tag); return this; }
Builder minYear(int year) { this.minYear = year; return this; }
Builder pageSize(int size) { this.pageSize = size; return this; }
SearchQuery build() {
if (text.isBlank()) throw new IllegalArgumentException("text");
if (pageSize < 1) throw new IllegalArgumentException("pageSize");
return new SearchQuery(this);
}
}
}The product is . Tags are copied. A caller cannot mutate the list behind the query after build().
Builder is not Factory Method
Builder
How a particular object is assembled. Many optional fields, real order, or boolean soup.
Factory Method
Which class is born. Factory Method hides new Car().
Both
A ReportFactory returns a PdfReportBuilder or HtmlReportBuilder. Factory chooses the family; Builder runs the steps. See Abstract Factory.
When to use Builder
Many fields are optional
Construction has a real order
Boolean soup
A builder for two required fields
Money.builder().paise(100).currency("INR").build() is extra ceremony. new Money(100, "INR") is the design. Builder is worth using when optional parts and validation would drown a constructor.
Setters on the product after build()
If SearchQuery still has setPageSize, you built a mutable bag. The product should be immutable. Change means a new builder from a copy, or a new build.
Lombok-style builders on every entity
Generating a builder for Ticket, Spot, and Money hides which fields are actually required. Interviewers notice when every type has .builder() and none of them validate. Use Builder where construction is the hard part. Use a constructor where two values are the whole story. That is the same instinct as value objects: small types stay small.
build() is allowed to refuse
minYear after maxYear, page size zero, blank text — fail before the product exists. That is cheaper than a SearchQuery that later throws in the repository. Validation belongs at the gate, not in every caller.
SearchQuery build() {
if (text.isBlank()) throw new IllegalArgumentException("text");
if (pageSize < 1 || pageSize > 100) throw new IllegalArgumentException("pageSize");
if (minYear != null && maxYear != null && minYear > maxYear) {
throw new IllegalArgumentException("year range");
}
return new SearchQuery(this);
}A parking receipt that should not be a builder
Receipt(ticketId, fee, duration) is three required fields. Use a constructor. SearchQuery with eight optional filters is the opposite. If you find yourself writing builder() on Money, TicketId, and SpotId, you are hiding requiredness. Interviewers notice extra ceremony.
// Honest constructor — do not builder this.
record Receipt(TicketId id, Money fee, Duration stay) {
Receipt {
Objects.requireNonNull(id);
Objects.requireNonNull(fee);
if (stay.isNegative()) throw new IllegalArgumentException("stay");
}
}