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.

Watch it evolve1 / 9
The problem

A print job with seven settings

PrintJob's constructor takes all seven settings, in order. A shorter one covers the plainest jobs.

80
Start
Stage 1
Final

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.

The first versionJava
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());
    }
}
PrintJob− title: String− pages: int− paper: String− color: boolean− duplex: boolean− copies: int− binding: String~ describe(): String

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.

Which boolean is which?Java
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.

Java
    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);
        }
    }
«create»«create»PrintJob− title: String− pages: int− paper: String− color: boolean− duplex: boolean− copies: int− binding: String~ named(String): Builder~ describe(): StringBuilder− title: String− pages: int− paper: String− color: boolean− duplex: boolean− copies: int− binding: String~ pages(int): Builder~ paper(String): Builder~ color(): Builder~ doubleSided(): Builder~ copies(int): Builder~ saddleStitched(): Builder~ build(): PrintJob
The calls read like the orderJava
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.

«create»«create»PrintJob− title: String− pages: int− paper: String− color: boolean− duplex: boolean− copies: int− binding: String~ named(String): Builder~ describe(): StringBuilder− title: String− pages: int− paper: String− color: boolean− duplex: boolean− copies: int− binding: String~ pages(int): Builder~ paper(String): Builder~ color(): Builder~ doubleSided(): Builder~ copies(int): Builder~ saddleStitched(): Builder~ build(): PrintJob

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

PrintJob.Builder

Collects settings through named steps; builds and validates

Product

PrintJob

The finished object, created only by the builder

Client

The order desk's code

Chains the steps it needs and calls build()

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() beats duplex(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 {.pages = 24, .color = true}

Rules must be checked before the object exists

JavaScript

An options object: new PrintJob({ title: 'Flyer', color: true })

Steps are added in several places before building

Python

Keyword arguments, often keyword-only, with checks in __post_init__

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

1. Why does .color() prevent the flyer mistake better than .color(true)?
2. Where should the rule "saddle stitching needs a multiple of four pages" live?
3. A class has three required settings and no optional ones. Is a builder worth it?
4. What's the idiomatic Python answer to seven optional settings?

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.