Builder
In short: Builder assembles a complex object step by step, with a name for every field, validates it in build() and returns a finished immutable object.
Problem
Nine parameters and two swapped words
The shop sends an email when an order ships. The email has nine fields: who it goes to, the address for replies, the subject, the body, a copy, a blind copy, an attachment, whether it is important and whether opening it is tracked.
class Email { private final String to; private final String replyTo; private final String subject; private final String body; private final String cc; private final String bcc; private final String attachment; private final boolean highPriority; private final boolean trackOpens;
public Email(String to, String replyTo, String subject, String body, String cc, String bcc, String attachment, boolean highPriority, boolean trackOpens) { this.to = to; this.replyTo = replyTo; this.subject = subject; this.body = body; this.cc = cc; this.bcc = bcc; this.attachment = attachment; this.highPriority = highPriority; this.trackOpens = trackOpens; }
public void send() { System.out.println("To: " + to); if (replyTo != null) { System.out.println("Reply to: " + replyTo); } System.out.println("Subject: " + subject); if (attachment != null) { System.out.println("Attachment: " + attachment); } if (highPriority) { System.out.println("High priority"); } }}This is how the confirmation for order 1042 is sent:
Email email = new Email("support@shop.example", "maria@example.com", "Order 1042 has shipped", "Your parcel is on its way.", null, null, "invoice-1042.pdf", true, false);email.send();To: support@shop.exampleReply to: maria@example.comSubject: Order 1042 has shippedAttachment: invoice-1042.pdfHigh priorityThe email went to the shop’s support, and Maria got nothing. The first two arguments are swapped. Both are text, so the compiler has no way to tell. And which is true and which is false at the end? To find out, you count positions in the constructor. Two null hold the places of fields this email doesn’t have.
Every new field, say the language of the email, changes the constructor and every place that calls it.
The idea
Builder: by name, in steps, with a check
The object isn’t made with one call of nine positions, but in steps, each with a name: .to(…), .subject(…). The steps go to a separate object, the builder. It collects the values, and at the end build():
- checks the required fields (here: recipient, subject and body) and throws an exception if one is missing;
- makes an immutable object: after that the email can’t change.
This is the Builder pattern. The roles:
- the product:
Email, immutable, with a private constructor, so there is no other way to make one; - the builder:
Email.Builder, with a method for every field. The method returns the builder itself, so the calls line up one after another in a chain; build(): the only place where the product is checked and made.
See it
The chain and build()
class Email { private final String to; private final String replyTo; private final String subject; private final String body; private final String cc; private final String bcc; private final String attachment; private final boolean highPriority; private final boolean trackOpens;
private Email(Builder b) { to = b.to; replyTo = b.replyTo; subject = b.subject; body = b.body; cc = b.cc; bcc = b.bcc; attachment = b.attachment; highPriority = b.highPriority; trackOpens = b.trackOpens; }
public boolean highPriority() { return highPriority; }
public void send() { System.out.println("To: " + to); if (replyTo != null) { System.out.println("Reply to: " + replyTo); } System.out.println("Subject: " + subject); if (attachment != null) { System.out.println("Attachment: " + attachment); } if (highPriority) { System.out.println("High priority"); } }
static class Builder { private String to; private String replyTo; private String subject; private String body; private String cc; private String bcc; private String attachment; private boolean highPriority; private boolean trackOpens;
public Builder to(String value) { to = value; return this; } public Builder replyTo(String value) { replyTo = value; return this; } public Builder subject(String value) { subject = value; return this; } public Builder body(String value) { body = value; return this; } public Builder cc(String value) { cc = value; return this; } public Builder bcc(String value) { bcc = value; return this; } public Builder attachment(String value) { attachment = value; return this; } public Builder highPriority() { highPriority = true; return this; } public Builder trackOpens() { trackOpens = true; return this; }
public Email build() { if (to == null) { throw new IllegalStateException("Missing recipient"); } if (subject == null) { throw new IllegalStateException("Missing subject"); } if (body == null) { throw new IllegalStateException("Missing body"); } return new Email(this); } }}A builder costs code: every field is written three times: in the email, in the builder and in its method. That’s why it isn’t made for every class.
Email.Builder is a nested class: it sees the private constructor of Email, and nobody else does. The fields of Email are final.Email email = new Email.Builder() .to("maria@example.com") .subject("Order 1042 has shipped") .body("Your parcel is on its way.") .attachment("invoice-1042.pdf") .highPriority() .build();email.send();
Email.Builder draft = new Email.Builder() .to("peter@example.com") .body("Your parcel is on its way.");try { draft.build();} catch (IllegalStateException e) { System.out.println(e.getMessage());}To: maria@example.comSubject: Order 1042 has shippedAttachment: invoice-1042.pdfHigh priorityMissing subjectEmail email = new Email.Builder() .to("maria@example.com") .subject("Order 1042 has shipped") .body("Your parcel is on its way.") .attachment("invoice-1042.pdf") .highPriority() .build();email.send();
Email.Builder draft = new Email.Builder() .to("peter@example.com") .body("Your parcel is on its way.");try { draft.build();} catch (IllegalStateException e) { System.out.println(e.getMessage());}Step by step, in words
- new Email.Builder(): an empty blueprint with nine slots. The three required ones (to, subject, body) are marked.
- .to(…) fills one slot and returns the same builder, so the chain goes on.
- Every call has a name: a value cannot land in the wrong slot, as it did with the positions in the constructor.
- .body(…): all three required slots are filled.
- Optional fields are written only when needed. highPriority() takes no argument: no more true in eighth place.
- build() checks the required slots and stamps out an Email. Its fields are final: the object doesn't change any more.
- The email goes to Maria, with the invoice, as high priority.
- A new builder, a new empty blueprint. Here someone forgot the subject.
- draft.build() reaches the empty slot subject and stops with an exception. An email without a subject never exists at all.
- The error says what is missing. Nothing was sent.
The email without a subject never reaches the mail: build() stops it already at assembly.
See it
Without a builder: what the language has
record writes the fields, the constructor and the accessors, and the object is immutable, but nine parameters are still nine positions. So the common cases get a factory method with a name (as in the Factory lesson), and the check goes into the compact constructor:record Email(String to, String replyTo, String subject, String body, String cc, String bcc, String attachment, boolean highPriority, boolean trackOpens) { Email { if (to == null || subject == null || body == null) { throw new IllegalArgumentException("Missing recipient, subject or body"); } }
static Email orderShipped(String to, int orderId, String invoice) { return new Email(to, "support@shop.example", "Order " + orderId + " has shipped", "Your parcel is on its way.", null, null, invoice, false, false); }}Email email = Email.orderShipped("maria@example.com", 1042, "invoice-1042.pdf");System.out.println(email.to() + " / " + email.subject() + " / " + email.attachment());maria@example.com / Order 1042 has shipped / invoice-1042.pdfHttpRequest.newBuilder().uri(…).header(…).build(). The Lombok library writes the builder for you with the @Builder annotation.When a builder is still worth it:
- the email is put together in steps, in different parts of the code: the attachment is added only if there is an invoice, the copy only for companies;
- there is a check across fields: for example, the blind copy can’t be the recipient’s address;
- there are many fields, most of them optional, and the language has no named arguments.
Try it
Predict the output
One builder, two calls to build(). What does the program print?
Email.Builder builder = new Email.Builder() .to("maria@example.com") .subject("Order 1042 has shipped") .body("Your parcel is on its way.");Email first = builder.build();builder.highPriority();Email second = builder.build();System.out.println(first.highPriority() + " " + second.highPriority());Try it
Find the bug
The shop sends the emails for shipped orders in a loop. Only the first order has an invoice:
List<Order> orders = List.of( new Order("maria@example.com", 1042, "invoice-1042.pdf"), new Order("peter@example.com", 1043, null));Email.Builder builder = new Email.Builder().replyTo("support@shop.example");for (Order order : orders) { builder.to(order.email()) .subject("Order " + order.id() + " has shipped") .body("Your parcel is on its way."); if (order.invoice() != null) { builder.attachment(order.invoice()); } builder.build().send();}To: maria@example.comReply to: support@shop.exampleSubject: Order 1042 has shippedAttachment: invoice-1042.pdfTo: peter@example.comReply to: support@shop.exampleSubject: Order 1043 has shippedAttachment: invoice-1042.pdfPeter got Maria’s invoice. Which line has the bug?
Pick the line with the bug.
The builder is made once, before the loop. It remembers everything it was told, and the attachment from the first email stays for the second, because nobody cleared it. A builder is for one assembly: make a new one inside the loop, for every email. Here the bug leaks personal data: someone else’s invoice with a name and an address.
The idea
When you don’t need it
Builder doubles the code of the class. It isn’t worth it when:
- there are two or three fields and all are required: a plain constructor is clearer;
- a record with a factory method for the common cases is enough;
- the object has nothing to check and isn’t put together in steps.
It is worth it when there are many fields and most are optional, when there is a check across them, or when the object is put together in steps, in different parts of the code.
Exercise
A parcel for the courier
Solve the task in your editor.
A builder for a parcel. The courier wants a parcel with a recipient, a phone, an address and a weight (required), and optionally: fragile and cash on delivery. Write Shipment and a builder for it. build() checks that the required fields are set and that the weight is over 0 and up to 30 kg. Make a parcel for Maria (2.5 kg, fragile, cash on delivery €42.90), print it, and try a 45 kg parcel:
Parcel for Maria, 0888 123 456Address: 5 Shipka St, PlovdivWeight: 2.5 kg, fragileCash on delivery: €42.90The weight must be over 0 and up to 30 kgMore tasks
- Fix the loop. Fix the program from "Find the bug" so that every email carries only its own invoice. The reply address must stay for all emails.
- A copy with one change. Add a method that turns a finished email into a builder with the same values (in Java it is often called
toBuilder()). Send the same email to another recipient. - The language’s way. Write
Shipmentas in the card "Without a builder" and compare the two versions. Which one is shorter, and what do you lose?
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Nested classes · record, switch and patterns · Design patterns · Factory Method and Simple Factory
- Continues in: The shop: which pattern where
- Tasks on this topic: Design patterns