Skip to content
wannacode

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
{
public string To { get; }
public string ReplyTo { get; }
public string Subject { get; }
public string Body { get; }
public string Cc { get; }
public string Bcc { get; }
public string Attachment { get; }
public bool HighPriority { get; }
public bool TrackOpens { get; }
public Email(string to, string replyTo, string subject, string body, string cc,
string bcc, string attachment, bool highPriority, bool trackOpens)
{
To = to;
ReplyTo = replyTo;
Subject = subject;
Body = body;
Cc = cc;
Bcc = bcc;
Attachment = attachment;
HighPriority = highPriority;
TrackOpens = trackOpens;
}
public void Send()
{
Console.WriteLine($"To: {To}");
if (ReplyTo != null)
{
Console.WriteLine($"Reply to: {ReplyTo}");
}
Console.WriteLine($"Subject: {Subject}");
if (Attachment != null)
{
Console.WriteLine($"Attachment: {Attachment}");
}
if (HighPriority)
{
Console.WriteLine("High priority");
}
}
}

This is how the confirmation for order 1042 is sent:

var 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();
Output
To: support@shop.example
Reply to: maria@example.com
Subject: Order 1042 has shipped
Attachment: invoice-1042.pdf
High priority

The 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
{
public string To { get; private init; }
public string ReplyTo { get; private init; }
public string Subject { get; private init; }
public string Body { get; private init; }
public string Cc { get; private init; }
public string Bcc { get; private init; }
public string Attachment { get; private init; }
public bool HighPriority { get; private init; }
public bool TrackOpens { get; private init; }
private Email()
{
}
public void Send()
{
Console.WriteLine($"To: {To}");
if (ReplyTo != null)
{
Console.WriteLine($"Reply to: {ReplyTo}");
}
Console.WriteLine($"Subject: {Subject}");
if (Attachment != null)
{
Console.WriteLine($"Attachment: {Attachment}");
}
if (HighPriority)
{
Console.WriteLine("High priority");
}
}
public class Builder
{
private string to;
private string replyTo;
private string subject;
private string body;
private string cc;
private string bcc;
private string attachment;
private bool highPriority;
private bool 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 InvalidOperationException("Missing recipient");
}
if (subject == null)
{
throw new InvalidOperationException("Missing subject");
}
if (body == null)
{
throw new InvalidOperationException("Missing body");
}
return new Email
{
To = to,
ReplyTo = replyTo,
Subject = subject,
Body = body,
Cc = cc,
Bcc = bcc,
Attachment = attachment,
HighPriority = highPriority,
TrackOpens = trackOpens,
};
}
}
}

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 can use the private init of the properties and the private constructor, and nobody else can.
var 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();
var draft = new Email.Builder()
.To("peter@example.com")
.Body("Your parcel is on its way.");
try
{
draft.Build();
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
Output
To: maria@example.com
Subject: Order 1042 has shipped
Attachment: invoice-1042.pdf
High priority
Missing subject

var 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();
var draft = new Email.Builder()
.To("peter@example.com")
.Body("Your parcel is on its way.");
try
{
draft.Build();
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
Step by step, in words
  1. new Email.Builder(): an empty blueprint with nine slots. The three required ones (To, Subject, Body) are marked.
  2. .To(…) fills one slot and returns the same builder, so the chain goes on.
  3. Every call has a name: a value cannot land in the wrong slot, as it did with the positions in the constructor.
  4. .Body(…): all three required slots are filled.
  5. Optional fields are written only when needed. HighPriority() takes no argument: no more true in eighth place.
  6. Build() checks the required slots and stamps out an Email. Its fields are private init: the object doesn't change any more.
  7. The email goes to Maria, with the invoice, as high priority.
  8. A new builder, a new empty blueprint. Here someone forgot the subject.
  9. draft.Build() reaches the empty slot Subject and stops with an exception. An email without a subject never exists at all.
  10. 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

C# has what Builder does in the language itself. init lets a property be set only at creation, required makes the compiler check the required ones, and the object initializer writes each one by name:
class Email
{
public required string To { get; init; }
public string ReplyTo { get; init; }
public required string Subject { get; init; }
public required string Body { get; init; }
public string Cc { get; init; }
public string Bcc { get; init; }
public string Attachment { get; init; }
public bool HighPriority { get; init; }
public bool TrackOpens { get; init; }
}
var email = new Email
{
To = "maria@example.com",
Subject = "Order 1042 has shipped",
Body = "Your parcel is on its way.",
Attachment = "invoice-1042.pdf",
HighPriority = true,
};
Console.WriteLine($"{email.To} / {email.Subject} / {email.HighPriority}");
Output
maria@example.com / Order 1042 has shipped / True
A forgotten subject and a change after creation are compile errors:
var email = new Email { To = "maria@example.com", Body = "Your parcel is on its way." };
email.To = "peter@example.com";
Error
error CS9035: Required member 'Email.Subject' must be set in the object initializer or attribute constructor.
error CS8852: Init-only property or indexer 'Email.To' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
If the class has a constructor, C# also calls it with named arguments: new Email(to: "…", replyTo: "…"). Then the swap from the first card doesn’t go unnoticed either.

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 assembly has to be passed from one place to another before it is ready.

Try it

Predict the output

One builder, two calls to Build(). What does the program print?

var 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();
Console.WriteLine($"{first.HighPriority} {second.HighPriority}");
Answer

Try it

Find the bug

The shop sends the emails for shipped orders in a loop. Only the first order has an invoice:

var orders = new List<Order>
{
new("maria@example.com", 1042, "invoice-1042.pdf"),
new("peter@example.com", 1043, null),
};
var builder = new Email.Builder().ReplyTo("support@shop.example");
foreach (var order in 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();
}
Output
To: maria@example.com
Reply to: support@shop.example
Subject: Order 1042 has shipped
Attachment: invoice-1042.pdf
To: peter@example.com
Reply to: support@shop.example
Subject: Order 1043 has shipped
Attachment: invoice-1042.pdf

Peter got Maria’s invoice. Which line has the bug?

Pick the line with the bug.

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;
  • required and init with an object initializer do the same without a second class;
  • 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:

Output
Parcel for Maria, 0888 123 456
Address: 5 Shipka St, Plovdiv
Weight: 2.5 kg, fragile
Cash on delivery: €42.90
The weight must be over 0 and up to 30 kg

More 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 Shipment as in the card "Without a builder" and compare the two versions. Which one is shorter, and what do you lose?

Test

Check yourself

1. new Email("support@…", "maria@…", …) compiles, and the email goes somewhere else. Why didn’t the compiler help?
2. What does build() do?
3. The builder is made before the loop and used for every email. The second email got the attachment of the first one. Why?
4. When is Builder still worth it?
5. What in C# does nearly the same as Builder, without a second class?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·