Low-level designCore8 min
Builder
When an object has many settings, most of them optional, build it step by step through a separate object with a named method for each setting, then check the whole and create it in one final call.
A print job with seven settings
PrintJob's constructor takes all seven settings, in order. A shorter one covers the plainest jobs.
1 · The situation
A print order with seven settings
A print shop's order desk enters jobs into a small app. A PrintJob has a title, a page count, a paper size, color or black and white, one side or both, a number of copies, and a binding. Most jobs leave most of these at their usual values. The first version has one constructor that takes all seven, and a shorter one for the plainest jobs.
final class PrintJob {
private final String title;
private final int pages;
private final String paper;
private final boolean color;
private final boolean duplex;
private final int copies;
private final String binding;
PrintJob(String title, int pages, String paper, boolean color, boolean duplex, int copies,
String binding) {
this.title = title;
this.pages = pages;
this.paper = paper;
this.color = color;
this.duplex = duplex;
this.copies = copies;
this.binding = binding;
}
PrintJob(String title, int pages, int copies) {
this(title, pages, "A4", false, false, copies, "none");
}
String describe() {
String ink = color ? "color" : "black and white";
String sides = duplex ? "double-sided" : "single-sided";
return title + ": " + pages + " pages " + paper + ", " + ink + ", " + sides + ", "
+ copies + " copies" + (binding.equals("saddle") ? ", saddle-stitched" : "");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new PrintJob("Zine", 24, "A5", true, true, 50, "saddle").describe());
// The desk wanted a color flyer, printed on one side.
System.out.println(new PrintJob("Flyer", 2, "A4", false, true, 500, "none").describe());
System.out.println(new PrintJob("Menu", 4, 20).describe());
PrintJob program = new PrintJob("Program", 30, "A5", false, true, 120, "saddle");
System.out.println(program.describe());
}
}The class diagram of this version, drawn from its code: one class with seven fields.
Zine: 24 pages A5, color, double-sided, 50 copies, saddle-stitched Flyer: 2 pages A4, black and white, double-sided, 500 copies Menu: 4 pages A4, black and white, single-sided, 20 copies Program: 30 pages A5, black and white, double-sided, 120 copies, saddle-stitched
2 · The change
Two jobs that came out wrong
The output hides two mistakes. The desk asked for a color flyer printed on one side, and got black and white on both sides: the call passed false, true where it meant true, false. Two booleans side by side read the same either way, and the compiler can't tell them apart.
final class PrintJob {
private final String title;
private final int pages;
private final String paper;
private final boolean color;
private final boolean duplex;
private final int copies;
private final String binding;
PrintJob(String title, int pages, String paper, boolean color, boolean duplex, int copies,
String binding) {
this.title = title;
this.pages = pages;
this.paper = paper;
this.color = color;
this.duplex = duplex;
this.copies = copies;
this.binding = binding;
}
PrintJob(String title, int pages, int copies) {
this(title, pages, "A4", false, false, copies, "none");
}
String describe() {
String ink = color ? "color" : "black and white";
String sides = duplex ? "double-sided" : "single-sided";
return title + ": " + pages + " pages " + paper + ", " + ink + ", " + sides + ", "
+ copies + " copies" + (binding.equals("saddle") ? ", saddle-stitched" : "");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new PrintJob("Zine", 24, "A5", true, true, 50, "saddle").describe());
// The desk wanted a color flyer, printed on one side.
System.out.println(new PrintJob("Flyer", 2, "A4", false, true, 500, "none").describe());
System.out.println(new PrintJob("Menu", 4, 20).describe());
PrintJob program = new PrintJob("Program", 30, "A5", false, true, 120, "saddle");
System.out.println(program.describe());
}
}The second mistake is worse. Saddle stitching folds sheets in half and staples them through the fold, so the page count must be a multiple of four. The 30-page program was accepted anyway; the shop finds out at the stapler. There's no good place to check: the constructor would have to reject it, and the shorter constructors would each need to remember to.
Problem | Cause |
|---|---|
Arguments in the wrong order | Seven positional parameters, two of them booleans next to each other |
Every call names every setting | Only the plainest job has a short constructor; another default means another overload |
Invalid jobs accepted | No step where the whole job is checked before it exists |
This is the telescoping constructor: one constructor per combination of optional settings, each longer than the last. The practice checks find nothing wrong, and they're right that the class is sound; the trouble is in every call site.
3 · The fix
Name each setting, then check and build
Move construction into a separate object that collects settings one named call at a time:
PrintJob.named(title) starts a Builder with every setting at its usual value. Each step sets one thing and returns the builder, so calls chain: .pages(24).paper("A5").color(). A step like color() takes no boolean at all, so there's nothing to swap.
static final class Builder {
private final String title;
private int pages = 1;
private String paper = "A4";
private boolean color;
private boolean duplex;
private int copies = 1;
private String binding = "none";
private Builder(String title) {
this.title = title;
}
Builder pages(int pages) {
this.pages = pages;
return this;
}
Builder paper(String paper) {
this.paper = paper;
return this;
}
Builder color() {
this.color = true;
return this;
}
Builder doubleSided() {
this.duplex = true;
return this;
}
Builder copies(int copies) {
this.copies = copies;
return this;
}
Builder saddleStitched() {
this.binding = "saddle";
return this;
}
PrintJob build() {
if (binding.equals("saddle") && pages % 4 != 0) {
throw new IllegalStateException(
title + ": saddle stitching needs a multiple of 4 pages, got " + pages);
}
return new PrintJob(this);
}
}final class PrintJob {
private final String title;
private final int pages;
private final String paper;
private final boolean color;
private final boolean duplex;
private final int copies;
private final String binding;
private PrintJob(Builder b) {
this.title = b.title;
this.pages = b.pages;
this.paper = b.paper;
this.color = b.color;
this.duplex = b.duplex;
this.copies = b.copies;
this.binding = b.binding;
}
static Builder named(String title) {
return new Builder(title);
}
String describe() {
String ink = color ? "color" : "black and white";
String sides = duplex ? "double-sided" : "single-sided";
return title + ": " + pages + " pages " + paper + ", " + ink + ", " + sides + ", "
+ copies + " copies" + (binding.equals("saddle") ? ", saddle-stitched" : "");
}
static final class Builder {
private final String title;
private int pages = 1;
private String paper = "A4";
private boolean color;
private boolean duplex;
private int copies = 1;
private String binding = "none";
private Builder(String title) {
this.title = title;
}
Builder pages(int pages) {
this.pages = pages;
return this;
}
Builder paper(String paper) {
this.paper = paper;
return this;
}
Builder color() {
this.color = true;
return this;
}
Builder doubleSided() {
this.duplex = true;
return this;
}
Builder copies(int copies) {
this.copies = copies;
return this;
}
Builder saddleStitched() {
this.binding = "saddle";
return this;
}
PrintJob build() {
if (binding.equals("saddle") && pages % 4 != 0) {
throw new IllegalStateException(
title + ": saddle stitching needs a multiple of 4 pages, got " + pages);
}
return new PrintJob(this);
}
}
}
public class Main {
public static void main(String[] args) {
PrintJob zine = PrintJob.named("Zine").pages(24).paper("A5").color().doubleSided()
.copies(50).saddleStitched().build();
System.out.println(zine.describe());
PrintJob flyer = PrintJob.named("Flyer").pages(2).color().copies(500).build();
System.out.println(flyer.describe());
System.out.println(PrintJob.named("Menu").pages(4).copies(20).build().describe());
try {
PrintJob.named("Program").pages(30).paper("A5").doubleSided().copies(120)
.saddleStitched().build();
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
}
}Zine: 24 pages A5, color, double-sided, 50 copies, saddle-stitched Flyer: 2 pages A4, color, single-sided, 500 copies Menu: 4 pages A4, black and white, single-sided, 20 copies Refused: Program: saddle stitching needs a multiple of 4 pages, got 30
The flyer comes out as ordered, and the bad program is refused before it exists.
4 · The pattern
Builder: construction in named steps
Builder separates constructing a complex object from the object itself. The builder holds the settings while they're collected, so the product never exists half-built, and the final build() can validate rules that involve several settings at once.
The roles in the print shop's code: the builder collects the settings, and builds the product.
Role | In the print shop | Its job |
|---|---|---|
Builder |
| Collects settings through named steps; builds and validates |
Product |
| The finished object, created only by the builder |
Client | The order desk's code | Chains the steps it needs and calls |
Director, optional | Not needed here | A class that knows a standard sequence of steps, such as "a standard zine" |
The desk names only the settings it changes; the builder checks the whole and creates the job.
5 · What it costs
A second class that mirrors the first
- More code. The builder repeats every field of the product, and a new setting is added in both.
- Errors move to run time. Forgetting a required setting, or breaking a rule, fails in
build()rather than at compile time. - The builder is mutable. It changes with each step, so share finished products, not builders.
Skip the builder when an object has a few settings that are all required, or when the language gives you named arguments. Use it when there are many optional settings, rules that span several of them, or a product that should be immutable once made.
6 · Getting it right
Required up front, optional as steps, rules in build()
- Take required values at the start. The title goes into
named(title), so a job without one can't be built. - Give each step a meaning, not a type.
doubleSided()beatsduplex(true). - Validate in
build(), where every setting is known. - Make the product immutable. The builder is the only place settings change.
- Let
build()return a new object each time, so a builder can be reused for similar jobs.
This pattern matters most where a language has only positional arguments. Elsewhere, the language does much of the work:
Language | Native alternative | Builder still useful when |
|---|---|---|
Java | None: arguments are positional | There are more than a few optional settings |
C++ | Designated initializers for a plain struct, such as | Rules must be checked before the object exists |
JavaScript | An options object: | Steps are added in several places before building |
Python | Keyword arguments, often keyword-only, with checks in | Rarely; keyword arguments cover most cases |
7 · Look-alikes
Builders, factories and fluent calls
Several things look like builders, and builders look like other creational patterns.
Idea | What it does |
|---|---|
Builder | Assembles one complex object in steps, then builds it |
Telescoping constructors | One constructor per combination of optional settings: what Builder replaces |
Abstract Factory | Creates a family of matching objects, each in one call |
Factory Method | Lets a subclass choose which class to create |
Fluent interface | Any API whose calls chain; a builder is one, but so is a stream |
Prototype | Copies an existing object and adjusts the copy |
Chaining alone doesn't make a builder. A builder ends in a product that didn't exist before build(); a fluent stream or a string builder works on one object throughout.
8 · In practice
Builders in the standard library
Java's HTTP client uses one: HttpRequest.newBuilder() takes the address, headers, timeout and method one call at a time, and build() returns a request that's immutable and can be sent many times.
The practice checks recognize a Builder when a class has at least three methods that return the builder itself and a method that creates the product, and they add a hint if the product still has a public setter. Design the parking lot; a ticket with several optional details is a natural candidate.
Check yourself
4 questions
Takeaways
Remember this
- Builder collects an object's settings through named steps and creates it in one final call.
- It replaces telescoping constructors and long lists of positional arguments.
- Validate rules that span several settings in
build(), the only way to get the product. - Take required values up front and make the product immutable.
- Languages with named arguments or options objects need builders far less often.