Builder Pattern
Definitionβ
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. Instead of a telescoping constructor with many parameters, you build the object step-by-step.
Coffee Shop Exampleβ
A custom coffee order can have many optional parts: size, extra shots, milk type, syrup, toppings, and special instructions. A constructor with all these parameters would be unreadable β Builder solves this.
The Problem: Telescoping Constructorβ
// Don't do this
var coffee = new CustomCoffee(
"Latte", // name
CupSize.Large, // size
2, // extra shots
"Oat", // milk type
"Vanilla", // syrup
true, // whipped cream
false, // iced
"Extra hot" // special instructions
);
// Which parameter was "Oat" again? Hard to read, hard to maintain.
Implementationβ
public class CustomCoffee
{
public string Name { get; private set; } = "";
public CupSize Size { get; private set; } = CupSize.Medium;
public int ExtraShots { get; private set; }
public string MilkType { get; private set; } = "Whole";
public string? Syrup { get; private set; }
public bool WhippedCream { get; private set; }
public bool Iced { get; private set; }
public string? SpecialInstructions { get; private set; }
public List<string> Toppings { get; private set; } = new();
public override string ToString()
{
var parts = new List<string> { $"{Name} ({Size})" };
if (ExtraShots > 0) parts.Add($"+{ExtraShots} shots");
if (MilkType != "Whole") parts.Add($"{MilkType} milk");
if (Syrup is not null) parts.Add($"{Syrup} syrup");
if (WhippedCream) parts.Add("whipped cream");
if (Iced) parts.Add("iced");
if (Toppings.Count > 0) parts.Add($"topped with {string.Join(", ", Toppings)}");
if (SpecialInstructions is not null) parts.Add($"[{SpecialInstructions}]");
return string.Join(" | ", parts);
}
// Builder β nested class has access to private setters
public class Builder
{
private readonly CustomCoffee _coffee = new();
public Builder(string name)
{
_coffee.Name = name;
}
public Builder WithSize(CupSize size)
{
_coffee.Size = size;
return this;
}
public Builder WithExtraShots(int shots)
{
_coffee.ExtraShots = shots;
return this;
}
public Builder WithMilk(string milkType)
{
_coffee.MilkType = milkType;
return this;
}
public Builder WithSyrup(string syrup)
{
_coffee.Syrup = syrup;
return this;
}
public Builder WithWhippedCream()
{
_coffee.WhippedCream = true;
return this;
}
public Builder MakeIced()
{
_coffee.Iced = true;
return this;
}
public Builder WithTopping(string topping)
{
_coffee.Toppings.Add(topping);
return this;
}
public Builder WithSpecialInstructions(string instructions)
{
_coffee.SpecialInstructions = instructions;
return this;
}
public CustomCoffee Build()
{
// Validation
if (string.IsNullOrWhiteSpace(_coffee.Name))
throw new InvalidOperationException("Coffee name is required.");
if (_coffee.Iced && _coffee.SpecialInstructions == "Extra hot")
throw new InvalidOperationException("Iced coffee cannot be extra hot.");
return _coffee;
}
}
}
Usage β Fluent Builderβ
var coffee = new CustomCoffee.Builder("Latte")
.WithSize(CupSize.Large)
.WithExtraShots(2)
.WithMilk("Oat")
.WithSyrup("Vanilla")
.WithWhippedCream()
.WithTopping("Caramel Drizzle")
.WithTopping("Chocolate Shavings")
.WithSpecialInstructions("Extra hot")
.Build();
Console.WriteLine(coffee);
// Output: Latte (Large) | +2 shots | Oat milk | Vanilla syrup | whipped cream | topped with Caramel Drizzle, Chocolate Shavings | [Extra hot]
// Minimal order β only the required parameter
var simple = new CustomCoffee.Builder("Espresso")
.WithSize(CupSize.Small)
.Build();
// Output: Espresso (Small)
Director β Reusable Recipesβ
A Director defines standard construction sequences β like a barista following a recipe:
public class BaristaDirector
{
private readonly CustomCoffee.Builder _builder;
public BaristaDirector(CustomCoffee.Builder builder)
{
_builder = builder;
}
public CustomCoffee MakeSignatureMocha()
{
return _builder
.WithSize(CupSize.Large)
.WithExtraShots(1)
.WithMilk("Whole")
.WithSyrup("Chocolate")
.WithWhippedCream()
.WithTopping("Cocoa Powder")
.Build();
}
public CustomCoffee MakeIcedVanillaLatte()
{
return _builder
.WithSize(CupSize.Large)
.WithMilk("Almond")
.WithSyrup("Vanilla")
.MakeIced()
.Build();
}
}
// Usage
var director = new BaristaDirector(new CustomCoffee.Builder("Mocha"));
var signature = director.MakeSignatureMocha();
Record-Based Builder (Modern C#)β
Using init properties and with expressions, you can achieve a builder-like pattern without a separate Builder class:
public record CustomCoffeeOrder(
string Name,
CupSize Size = CupSize.Medium,
int ExtraShots = 0,
string MilkType = "Whole",
string? Syrup = null,
bool WhippedCream = false,
bool Iced = false
);
// Usage β named arguments act like a builder
var order = new CustomCoffeeOrder(
Name: "Latte",
Size: CupSize.Large,
MilkType: "Oat",
Syrup: "Vanilla",
WhippedCream: true
);
// Clone with modifications using 'with' expression
var icedVersion = order with { Iced = true, MilkType = "Almond" };
StringBuilder β .NET's Built-in Builderβ
System.Text.StringBuilder is a Builder pattern itself β you construct a string step-by-step instead of creating many intermediate string objects:
var receipt = new StringBuilder();
receipt.AppendLine("=== Coffee Receipt ===");
receipt.AppendLine($"Latte (Large) $4.00");
receipt.AppendLine($" + Oat milk $0.60");
receipt.AppendLine($" + Vanilla $0.50");
receipt.AppendLine($"-----------------------");
receipt.AppendLine($"Total: $5.10");
Console.WriteLine(receipt.ToString());
When to Useβ
- Object has many optional parameters or configuration steps
- You need immutable objects with many properties (combine with
initorrecord) - The construction process should support different representations (same steps, different results)
- You need validation before the object is fully created
When NOT to Useβ
- The object is simple (2-3 fields, all required) β use a constructor
- There's only one way to build the object β no need for step-by-step control
- The builder adds no validation or logic over a constructor β it's unnecessary overhead
Key Takeawaysβ
- Builder separates construction from representation β the same steps can produce different results
- Fluent API (
return this) makes the builder readable and chainable - A Director encapsulates common recipes β reuse standard configurations
- In modern C#,
recordtypes with named arguments andwithexpressions can replace builders for simpler cases StringBuilderis a real-world Builder you already use
Interview Questionsβ
Q: What's the difference between Builder and Factory Method? Factory Method focuses on which class to instantiate. Builder focuses on how to construct a complex object step-by-step. Factory returns a product in one call; Builder returns it after multiple steps.
Q: Do you always need a Director? No. Director is optional β use it only when you have standard recipes that should be reused. If every construction is unique, skip the Director and use the Builder directly.
Q: How does Builder relate to Immutable objects?
Builder is the standard way to construct immutable objects with many fields. The object has no setters; the Builder collects state and produces the final object in Build().
Related Topicsβ
- Abstract Factory β creates families, Builder constructs one complex object
- Prototype β clone instead of build
- Structs & Records β
recordtypes as a modern builder alternative