Design Patterns & Practices  

Learn the Builder Pattern in C#

This Article explains the Builder design pattern in C#, shows when to use it, and provides two working examples with code: a classic Builder (with Director) and a fluent-style Builder. Copy the code into your C# project and run to experiment.

What is the Builder Pattern?

The Builder pattern separates the construction of a complex object from its representation. It allows the same construction process to create different representations. It's useful when an object requires multiple steps to construct or when you want to create immutable objects with many optional parameters.

When to use

  1. When the construction of an object is complex, it should be independent of the parts that make up the object.

  2. When you want to create different representations of an object using the same construction process.

  3. When an object has many optional parameters, Builder helps avoid telescoping constructors.

Benefits

  • Improved readability of client code (fluent builders).

  • Encapsulates construction logic in a single place.

  • Makes object creation expressive and less error-prone.

Example 1. Classic Builder with Director (House)

This example builds a House step-by-step. There is an IHouseBuilder interface, a ConcreteHouseBuilder, a Director that defines construction sequences, and the final House product.


// Product
public class House
{
    public string Foundation { get; set; }
    public string Structure { get; set; }
    public string Roof { get; set; }
    public string Interior { get; set; }

    public override string ToString()
    {
        return $"House: Foundation={Foundation}, Structure={Structure}, Roof={Roof}, Interior={Interior}";
    }
}

// Builder interface
public interface IHouseBuilder
{
    void BuildFoundation();
    void BuildStructure();
    void BuildRoof();
    void BuildInterior();
    House GetHouse();
}

// Concrete builder
public class ConcreteHouseBuilder : IHouseBuilder
{
    private House house = new House();

    public void BuildFoundation()
    {
        house.Foundation = "Concrete and rebar";
    }

    public void BuildStructure()
    {
        house.Structure = "Brick and mortar";
    }

    public void BuildRoof()
    {
        house.Roof = "Tiles";
    }

    public void BuildInterior()
    {
        house.Interior = "Standard interior fit-out";
    }

    public House GetHouse()
    {
        var result = house;
        house = new House(); // reset for next build
        return result;
    }
}

// Director
public class Director
{
    private readonly IHouseBuilder builder;

    public Director(IHouseBuilder builder)
    {
        builder = builder;
    }

    // Example: build a simple house
    public void ConstructSimpleHouse()
    {
        builder.BuildFoundation();
        builder.BuildStructure();
        builder.BuildRoof();
        _builder.BuildInterior();
    }
}

// Usage
var builder = new ConcreteHouseBuilder();
var director = new Director(builder);
director.ConstructSimpleHouse();
House house = builder.GetHouse();
Console.WriteLine(house);

Notes on Example 1

The Director encapsulates the sequence of building steps. The client can either use the Director for standard construction flows or call the builder steps directly for custom sequences.

Example 2. Fluent Builder (Person)

A fluent builder provides a readable API using method chaining, and is commonly used to construct objects with many optional properties.


public class Person
{
    public string FirstName { get; }
    public string LastName { get; }
    public int Age { get; }
    public string Phone { get; }
    public string Address { get; }

    public Person(string firstName, string lastName, int age, string phone, string address)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        Phone = phone;
        Address = address;
    }

    public override string ToString()
    {
        return $"{FirstName} {LastName}, Age={Age}, Phone={Phone}, Address={Address}";
    }
}

public class PersonBuilder
{
    private string firstName = "";
    private string lastName = "";
    private int age = 0;
    private string phone = "";
    private string address = "";

    public PersonBuilder SetFirstName(string firstName) { firstName = firstName; return this; }
    public PersonBuilder SetLastName(string lastName) { lastName = lastName; return this; }
    public PersonBuilder SetAge(int age) { age = age; return this; }
    public PersonBuilder SetPhone(string phone) { phone = phone; return this; }
    public PersonBuilder SetAddress(string address) { address = address; return this; }

    public Person Build()
    {
        return new Person(_firstName, lastName, age, phone, address);
    }
}

// Usage
var person = new PersonBuilder()
    .SetFirstName("John")
    .SetLastName("Doe")
    .SetAge(30)
    .SetPhone("555-1234")
    .SetAddress("123 Main St")
    .Build();

Console.WriteLine(person);

When NOT to use Builder

  • For simple objects with few parameters, using a builder adds unnecessary complexity.

  • If the construction process is trivial and unlikely to change.

Quick Tips

  • For immutable objects, have the builder construct the final immutable type.

  • Consider using optional parameters or object initializers for simple cases.

  • Use fluent builders for readability in client code.

  • Keep builders focused on construction; avoid putting complex business logic inside them.