RO EN

Builder Pattern – step-by-step construction of complex objects

Builder Pattern – step-by-step construction of complex objects
Doru Bulubasa
07 November 2025

When you create complex objects (many properties, initialization steps, or optional configurations), long constructor calls (new Complex(a, b, c, d, e)) become hard to read and maintain. The Builder Pattern separates the process of creating a complex object from its representation, allowing step-by-step construction and providing a fluent, easy-to-use API.


🔍 What Builder Pattern solves

  • Constructors with many parameters (or optional parameters) → hard-to-read code.

  • The need to create different variants of the same type (e.g., object with default settings vs. object with custom settings).

  • Stepwise initialization (e.g., validations, dependent settings, additional steps).

The builder offers a fluent API (methods that can be "chained" one after another), facilitating code readability and testing.


⚙️ Practical example in C# — fluent construction of an order (Order)

Imagine a complex Order class (many options, address, items, discounts):

public class Order
{
    public Guid Id { get; init; }
    public string CustomerName { get; init; }
    public string ShippingAddress { get; init; }
    public List<OrderItem> Items { get; init; } = new();
    public decimal Discount { get; init; }
    public DateTime CreatedAt { get; init; }
}

public record OrderItem(string Sku, int Quantity, decimal Price);

The builder:

public class OrderBuilder
{
    private readonly Order _order = new()
    {
        Id = Guid.NewGuid(),
        CreatedAt = DateTime.UtcNow
    };

    public OrderBuilder ForCustomer(string name)
    {
        _order = _order with { CustomerName = name };
        return this;
    }

    public OrderBuilder ShipTo(string address)
    {
        _order = _order with { ShippingAddress = address };
        return this;
    }

    public OrderBuilder AddItem(string sku, int qty, decimal price)
    {
        var items = new List<OrderItem>(_order.Items) { new(sku, qty, price) };
        _order = _order with { Items = items };
        return this;
    }

    public OrderBuilder WithDiscount(decimal discount)
    {
        _order = _order with { Discount = discount };
        return this;
    }

    public Order Build()
    {
        // final validations
        if (string.IsNullOrWhiteSpace(_order.CustomerName))
            throw new InvalidOperationException("Customer is required");

        return _order;
    }
}

Fluent usage:

var order = new OrderBuilder()
    .ForCustomer("Ion Popescu")
    .ShipTo("Str. Exemplu 10, Bucharest")
    .AddItem("SKU-123", 2, 49.9m)
    .AddItem("SKU-456", 1, 19.99m)
    .WithDiscount(5m)
    .Build();

The code is clear, each step is explicit, and validations are centralized in the Build() method.


✅ Advantages

  • Clarity: easy-to-read code (especially for objects with many parameters).

  • Extensibility: easy to add new options without complicating constructors.

  • Immutability: combined with record/init you can have ergonomically built immutable objects.

  • Testability: you can quickly build test instances with only the necessary steps.


⚠️ Disadvantages / when to avoid

  • Introduces extra classes (boilerplate) — may seem oversized for simple objects.

  • If you don't have many options/variants, simple new can be more concise.

  • Can be misused for logic that should be in services — the builder should only handle creation/initialization.


🔁 Builder vs Factory

  • Factory Method / Abstract Factory: focuses on choosing the type of object (which concrete class to instantiate).

  • Builder: focuses on internal construction of a complex object, step by step, independent of the concrete type.

The two can be combined: a factory can return different builders depending on the context.


🧠 Best practice in .NET

  • Use record + with for immutable objects and a builder that clones/sets properties.

  • Keep complex validations in Build() and not in individual methods.

  • Expose a simple creation variant (static OrderBuilder.CreateDefault()) for common cases.


🚀 Conclusion

Builder Pattern is an excellent tool when your objects become complex and you need an expressive API to build them. In .NET, combined with record and modern patterns, it leads to clear, testable, and easily extensible code. Use it when constructors start to become hard to read or when you want to offer step-by-step configuration options for your consumers.