Skip to content

// Design Patterns · 02

Builder

Many optional partsleads toBuild it step by step

Assemble a complex object step by step, then build it.

How it fits together

Builder structure«client»OrderTestsArrange()OrderBuilderWithCustomer(c)WithLine(sku, qty)WithDiscount(pct)Build()«record»Orderimmutableinvariants checkedchainsBuild()
uses / callscreates
  • OrderTests uses / calls OrderBuilder (chains)
  • OrderBuilder creates Order (Build())

Spot it

  • A constructor with eight parameters, half null
  • Telescoping constructor overloads
  • Tests repeat long setup for one object

It costs

A parallel class to maintain that drifts when the target changes.

In code

public sealed record Order(    Guid CustomerId,    IReadOnlyList<(string Sku, int Qty)> Lines,    decimal DiscountPct);​public sealed class OrderBuilder{    private Guid _customerId = Guid.NewGuid();    private readonly List<(string Sku, int Qty)> _lines = [];    private decimal _discountPct;​    public OrderBuilder WithCustomer(Guid id) { _customerId = id; return this; }​    public OrderBuilder WithLine(string sku, int qty) { _lines.Add((sku, qty)); return this; }​    public OrderBuilder WithDiscount(decimal pct) { _discountPct = pct; return this; }​    public Order Build() => _lines.Count > 0        ? new Order(_customerId, [.. _lines], _discountPct)        : throw new InvalidOperationException("An order needs at least one line.");}
Go deeperThe full explanation and what to ask for · 1 min readExpand

A builder separates how an object is assembled from the object itself. Parts are set in any order, and Build() is the one place that validates and produces a finished, usually immutable, result.

A builder for a three-field type is ceremony. Most languages cover simple cases with named arguments, optional fields or a copy-with-changes syntax, without a second class.

Standard libraries are full of builders: URI builders, string builders, host and configuration builders. In tests, a test data builder with good defaults keeps each test focused on the one field it cares about.

Ask for it

  • fluent builder
  • telescoping constructor
  • immutable result
  • test data builder
  • required fields

Add an OrderBuilder for tests with sensible defaults and fluent methods (WithCustomer, WithLine, WithDiscount). Build() returns an immutable Order and checks its invariants there. Replace the setup in OrderTests with it.