Builder Pattern, Explained the Right Way
A step-by-step guide to building complex objects cleanly
The Builder Pattern is a creational design pattern that allows you to construct complex objects step by step, while keeping the construction process separate from the final object representation.
In simple terms:
Instead of passing 10+ parameters into a constructor or writing many constructors for different combinations, you incrementally build the object.
You can chain optional steps, add defaults, and validate before producing the final object.
Key components:
Product – the object being created.
Builder – the class that knows how to build the object.
Fluent API / Method chaining – allows adding optional components step by step.
Build() – returns the final object, optionally after validation.
Why We Need Builder Pattern Here
Consider an Invoice object:
Mandatory fields: InvoiceNumber, Customer, IssuedOn
Optional fields: DueDate, Currency, LineItems, TaxRate, Discount, Notes
Without a builder, you might have:
var invoice = new Invoice(
invoiceNumber: "INV-1234",
issuedOn: DateTime.UtcNow,
dueDate: DateTime.UtcNow.AddDays(30),
customerName: "Brandon Sorp",
customerEmail: "billing@dev.com",
currency: "EUR",
lineItems: new List<LineItem> { ... },
taxRate: 19,
discount: 5,
notes: "Thank you!"
);
Problems:
Constructor is too long → hard to read.
Adding a new optional field (e.g.,
PaymentMethod) → need a new constructor or optional parameters.Mixing mandatory and optional fields → easy to make mistakes.
Builder solves all of this.
Applying the Builder Pattern to Invoice
To fix these problems, we move the construction logic out of the Invoice itself and into a dedicated builder.
The goal is simple:
Enforce mandatory fields
Allow optional fields to be added incrementally
Ensure the final object is always valid
Step 1: Define the Product (Invoice)
The Invoice class represents the final object.
Its constructor is kept private so it cannot be created in an invalid state.
public class Invoice
{
public string InvoiceNumber { get; private set; }
public DateTime IssuedOn { get; private set; }
public DateTime? DueDate { get; private set; }
public string CustomerName { get; private set; }
public string CustomerEmail { get; private set; }
public string Currency { get; private set; }
public List<LineItem> LineItems { get; private set; }
public decimal TaxRate { get; private set; }
public decimal Discount { get; private set; }
public string Notes { get; private set; }
private Invoice() { }
}
By doing this:
You prevent accidental misuse
You guarantee invoices are created only through the builder
Step 2: Create the Builder
The builder is responsible for:
Collecting data step by step
Providing sensible defaults
Validating business rules before object creation
public class InvoiceBuilder
{
private readonly Invoice _invoice = new Invoice();
public InvoiceBuilder(string invoiceNumber, string customerName, string customerEmail)
{
_invoice.InvoiceNumber = invoiceNumber;
_invoice.CustomerName = customerName;
_invoice.CustomerEmail = customerEmail;
_invoice.IssuedOn = DateTime.UtcNow;
_invoice.Currency = "EUR"; // default
_invoice.LineItems = new List<LineItem>();
}
public InvoiceBuilder WithDueDate(DateTime dueDate)
{
_invoice.DueDate = dueDate;
return this;
}
public InvoiceBuilder WithCurrency(string currency)
{
_invoice.Currency = currency;
return this;
}
public InvoiceBuilder AddLineItem(LineItem item)
{
_invoice.LineItems.Add(item);
return this;
}
public InvoiceBuilder WithTaxRate(decimal taxRate)
{
_invoice.TaxRate = taxRate;
return this;
}
public InvoiceBuilder WithDiscount(decimal discount)
{
_invoice.Discount = discount;
return this;
}
public InvoiceBuilder WithNotes(string notes)
{
_invoice.Notes = notes;
return this;
}
public Invoice Build()
{
Validate();
return _invoice;
}
private void Validate()
{
if (string.IsNullOrWhiteSpace(_invoice.InvoiceNumber))
throw new InvalidOperationException("Invoice number is required.");
if (!_invoice.LineItems.Any())
throw new InvalidOperationException("Invoice must have at least one line item.");
}
}
Step 3: Using the Builder
Now object creation becomes expressive and hard to misuse:
var invoice = new InvoiceBuilder(
invoiceNumber: "INV-1234",
customerName: "Brandon Sorp",
customerEmail: "billing@dev.com")
.WithDueDate(DateTime.UtcNow.AddDays(30))
.WithCurrency("EUR")
.AddLineItem(new LineItem { ... })
.WithTaxRate(19)
.WithDiscount(5)
.WithNotes("Thank you!")
.Build();
When to Use the Builder Pattern
Use the Builder Pattern when:
Objects have many optional properties
Construction involves business rules
Object creation should be readable and expressive
Avoid it for:
Simple DTOs
Objects with 2–3 fields and no validation
Where .NET Uses the Builder Pattern
The Builder Pattern is widely used in .NET to simplify construction of complex objects with multiple optional settings, providing readability, safety, and maintainability. Here are some key examples:
WebApplicationBuilder: Step-by-step configuration of services, middleware, logging, and hosting.
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Logging.AddConsole(); var app = builder.Build();ConfigurationBuilder: Combines multiple configuration sources (e.g., JSON files, environment variables, command-line args) into a single IConfiguration object.
var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .AddEnvironmentVariables() .Build();StringBuilder: Efficiently builds strings incrementally, avoiding the performance overhead of repeated concatenation.
var sb = new StringBuilder(); sb.Append("Hello, ") .Append("World!") .AppendLine(" Welcome to .NET."); string result = sb.ToString();