Skip to content

Builder Pattern

The problem

When an object has many optional fields, constructor calls become unreadable. A function with eight parameters forces callers to remember argument order, and most parameters are left as null or a default sentinel. The telescoping constructor antipattern is the common symptom: one constructor for every combination of optional fields, each delegating to a longer one. Adding a ninth parameter breaks every call site.

The Builder pattern moves construction into a dedicated object that collects configuration incrementally. The caller sets only what matters, in any order, and triggers construction in a single terminal step. That step is also the right place to validate that mandatory fields are present and that collected configuration is internally consistent. The product comes back in a fully initialized state or not at all.

Structure

classDiagram
class QueryBuilder {
-table: string
-columns: string
-conditions: string[]
-limitValue: number|null
-orderByColumn: string
+select(columns) QueryBuilder
+from(table) QueryBuilder
+where(condition) QueryBuilder
+orderBy(col, dir) QueryBuilder
+limit(n) QueryBuilder
+build() string
}

When to use

  • An object requires more than three or four constructor arguments, especially when most are optional.
  • You need to produce several representations of the same logical structure (e.g. SQL vs. a query DSL object vs. a log string) from the same assembly steps.
  • Construction involves validation that only makes sense after all inputs are known (cross-field invariants).
  • You want to prevent callers from holding a partially constructed object and accidentally using it.

Implementation

The SQL QueryBuilder accumulates clauses through method chaining: each setter returns this (or self), and build() validates then assembles the final string. Python’s from is a reserved keyword, so the method is named from_. Go has no default parameter values, so optional clauses use explicit fields; the hasLimit boolean distinguishes “not set” from LIMIT 0, and Build() returns an error rather than panicking.

idle
Click Run TS to execute. First run downloads Babel (~400 KB, cached after that).

Tradeoffs

ProCon
Eliminates telescoping constructorsMore types to maintain
Makes optional parameters explicitOverkill for objects with 2-3 fields
Prevents half-constructed objectsFluent interface can obscure invariants
Supports multiple representationsBuilder itself can grow unwieldy

Gotchas

  • Mutable builder, immutable product: build() should copy state rather than expose builder fields on the product. Callers who keep a reference to the builder and mutate it after building should not affect the product.
  • Validate in build(), not in setters: individual setters rarely have enough context to enforce cross-field invariants. Collect all inputs first, then validate once.
  • TypeScript this return type: returning this instead of the concrete class name enables subclassing. In Python, the QueryBuilder annotation on return types breaks if you subclass; use Self (3.11+) or a TypeVar bound to the class.
  • Go error accumulation: Go has no method chaining ergonomics for collecting errors across calls. Put all validation in Build() and return a single error from there.
  • Calling build() twice: two calls on the same builder should produce two independent products. Either document single-use or copy internal state in build() so subsequent mutations don’t affect earlier products.

References

  • Design Patterns, the full GoF catalog and pattern index
  • Factory, a simpler creational pattern for single-step construction
  • Strategy, a behavioral pattern that pairs well with builders for algorithm selection