Skip to main content

Command Palette

Search for a command to run...

Builder Pattern, Explained the Right Way

A step-by-step guide to building complex objects cleanly

Published
4 min read

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:

  1. Product – the object being created.

  2. Builder – the class that knows how to build the object.

  3. Fluent API / Method chaining – allows adding optional components step by step.

  4. 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:

  1. Constructor is too long → hard to read.

  2. Adding a new optional field (e.g., PaymentMethod) → need a new constructor or optional parameters.

  3. 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();