SwadhyAI Notes

All Low-Level Design notes

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. 1

    Required

    Builder constructor or mandatory setters that build() checks. Text cannot be blank.

  2. 2

    Optional

    Named fluent methods. Defaults live here (pageSize = 20). Order should not matter.

  3. 3

    build()

    Validate combinations. Copy collections. Return an immutable SearchQuery.

Named parts, one validation gate

java
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

Search filters, notification payloads, HTTP request objects. A constructor with eight nulls is how bugs hide. Name the parts the use case actually sets.

Construction has a real order

Start document, add section, seal. A builder can refuse build() until seal() ran. That is stronger than a comment on a public constructor.

Boolean soup

new Ticket(vehicle, true, false, true) does not say which flag is EV. Named methods do. If the flags are really rules, a specification or policy may be the better extract.

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.

java
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.

java
// 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");
  }
}

Related pages