Back to Design Pattern Course

Design Pattern Course

Builder is a Creational Design Pattern.

The Builder pattern was introduced to solve problems that come up with telescoping constructors and with Factory/Abstract Factory when an object has a lot of attributes — especially a mix of required and optional ones.

Telescoping constructors (a chain of overloaded constructors, each taking one more parameter than the last) and static factory methods both share the same limitation: they don't scale well to a large number of optional parameters. Callers either have to pass null/default values for the parameters they don't care about, or you end up maintaining a combinatorial explosion of overloaded constructors.

The Builder pattern solves this by separating the construction of an object from its representation, building it up step-by-step via chained setter-like methods, and only producing the final, fully-formed object with a terminal build() call. This also lets the constructed object be immutable, since all fields can be set once, in the builder, before the real object is ever created.

public class Employee { // All final fields — the resulting object is immutable private final int empId; private final String empName; private final double empSalary; private final String empAddress; private Employee(EmployeeBuilder builder) { this.empId = builder.empId; this.empName = builder.empName; this.empSalary = builder.empSalary; this.empAddress = builder.empAddress; } // Only getters — no setters, to preserve immutability public int getEmpId() { return empId; } public String getEmpName() { return empName; } public double getEmpSalary() { return empSalary; } public String getEmpAddress() { return empAddress; } public static class EmployeeBuilder { private int empId; private String empName; private double empSalary; private String empAddress; public EmployeeBuilder empId(int empId) { this.empId = empId; return this; } public EmployeeBuilder empName(String empName) { this.empName = empName; return this; } public EmployeeBuilder empSalary(double empSalary) { this.empSalary = empSalary; return this; } public EmployeeBuilder empAddress(String empAddress) { this.empAddress = empAddress; return this; } // Returns the fully constructed, immutable Employee object public Employee build() { return new Employee(this); } } @Override public String toString() { return "Employee [empId=" + empId + ", empName=" + empName + ", empSalary=" + empSalary + ", empAddress=" + empAddress + "]"; } public static void main(String[] args) { Employee emp = new Employee.EmployeeBuilder() .empId(149903) .empName("Praveen") .empSalary(1000000d) .empAddress("Hyderabad") .build(); System.out.println(emp); } }

Output

Employee [empId=149903, empName=Praveen, empSalary=1000000.0, empAddress=Hyderabad]

One gap in the example above worth calling out: as written, every field is optional and there's no validation. In a real system you'd typically enforce required fields either by accepting them as constructor parameters on the builder itself (new EmployeeBuilder(empId, empName)) or by validating in build() and throwing an IllegalStateException if a required field was never set — otherwise the Builder pattern can silently produce half-populated, "invalid" objects, which defeats part of the point of making the object immutable and well-formed in the first place.

Benefits

  • Solves the telescoping constructor problem — no need for a dozen overloaded constructors to cover every combination of optional parameters.
  • Produces immutable, fully-formed objects with no public setters.
  • Makes object construction self-documenting and readable at the call site (.empId(...).empName(...)) compared to a long, unlabeled constructor argument list.
  • Lets you validate the full set of fields together in build(), before the object is ever exposed in an invalid state — something constructors and setters can't easily guarantee.
  • Plays well with the Fluent Interface style (method chaining) and is the standard way to construct request/config objects in many modern libraries (e.g., the AWS SDK, OkHttp's Request.Builder).

Limitations

  • Extra boilerplate: you're essentially writing two classes (or one class plus a nested builder) for every object that needs this treatment, which adds complexity for simple objects with only a couple of fields.
  • Requires more upfront design — you need to decide what's required vs. optional and how to enforce that, rather than just adding a constructor parameter.
  • For Java specifically, this boilerplate is a known pain point; tools like Lombok's @Builder annotation generate the entire builder class for you, which is why hand-written builders like the one above are less common in modern Spring/Java codebases than they were a decade ago. Records (Java 16+) and Kotlin's named/default parameters reduce the need for Builder in some cases too, since they solve part of the same "too many constructor params" problem natively.

Builder Design Pattern Examples in the JDK

  • java.lang.StringBuilder#append() (and the legacy, synchronized StringBuffer#append()) — technically more of a fluent mutable-buffer API than a classic GoF Builder, but commonly cited as the idiom's spirit: chained calls that incrementally build up a result.
  • javax.xml.parsers.DocumentBuilder — a closer match to the classic pattern, used to build a DOM Document from XML.
  • java.util.Locale.Builder — builds an immutable Locale object step-by-step, very close to the classic GoF pattern shown above.
  • java.util.stream.Stream.Builder — builds an immutable Stream by adding elements one at a time, then calling build().