An order in an e-commerce system can have dozens of optional details: shipping address, billing address, discount code, gift wrap, special instructions. Put them all in constructors and you get the infamous telescoping constructor:

public Order(string customerName, string email) { } public Order(string customerName, string email, Address shippingAddress) { } public Order(string customerName, string email, Address shippingAddress, Address billingAddress) { } public Order(string customerName, string email, Address shippingAddress, Address billingAddress, string discountCode, bool giftWrap, string specialInstructions, PaymentMethod paymentMethod) { } 

Hard to read, easy to get wrong. The Builder pattern builds complex objects step by step, and keeps the rules in one place.

The idea

The Gang of Four put it like this:

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

The original version had a Director, an abstract Builder, ConcreteBuilders and a Product. Today almost everyone uses the lighter, fluent form: every method sets one thing and returns the builder, and Build() validates and creates the object.

A fluent builder that validates

public record EmailMessage { public required string To { get; init; } public required string Subject { get; init; } public string Body { get; init; } = ""; public bool IsHtml { get; init; } public Priority Priority { get; init; } = Priority.Normal; public IReadOnlyList<string> Attachments { get; init; } = []; } 
public class EmailMessageBuilder { private string? _to; private string? _subject; private string _body = ""; private bool _isHtml; private Priority _priority = Priority.Normal; private readonly List<string> _attachments = []; public EmailMessageBuilder To(string to) { _to = to; return this; } public EmailMessageBuilder Subject(string subject) { _subject = subject; return this; } public EmailMessageBuilder Body(string body) { _body = body; return this; } public EmailMessageBuilder AsHtml() { _isHtml = true; return this; } public EmailMessageBuilder WithPriority(Priority priority) { _priority = priority; return this; } public EmailMessageBuilder AttachFile(string path) { _attachments.Add(path); return this; } public EmailMessage Build() { // All the rules in one place if (!System.Net.Mail.MailAddress.TryCreate(_to, out _)) throw new InvalidOperationException("A valid recipient is required"); if (string.IsNullOrWhiteSpace(_subject)) throw new InvalidOperationException("The subject is required"); // A business rule the caller doesn't have to remember var priority = _body.Contains("URGENT", StringComparison.OrdinalIgnoreCase) ? Priority.High : _priority; return new EmailMessage { To = _to, Subject = _subject, Body = _body, IsHtml = _isHtml, Priority = priority, Attachments = _attachments.ToList() // a copy: the builder can't change the message later }; } } 
var email = new EmailMessageBuilder() .To("customer@example.com") .Subject("Welcome!") .Body("<h1>Welcome!</h1><p>Thanks for joining us.</p>") .AsHtml() .AttachFile("welcome_package.pdf") .Build(); 

Each call says what it sets, optional parts are simply skipped, and validation happens once, in Build().

Do you still need it? Records and required

Modern C# covers a lot of what builders used to do. With init and required members, an object initializer is safe and readable, and the compiler refuses to build a message without To and Subject:

var email = new EmailMessage { To = "customer@example.com", Subject = "Welcome!" }; 

So the rule is simple: use an object initializer for plain data, and a builder when construction has logic: validation across fields, business rules, defaults that depend on other values, or steps that need external services.

Builders and Dependency Injection

A builder can receive services from the container (a validator, a template engine, configuration). But builders are stateful: they collect values until Build(). Two rules follow:

  • register them as transient, never as singleton or scoped;
  • get a new builder for every object you build. A service that builds many objects shouldn't keep a single builder in a field: inject a Func<TBuilder> (or a small factory) and call it every time.
services.AddTransient<IApiRequestBuilder, ApiRequestBuilder>(); services.AddTransient<Func<IApiRequestBuilder>>(sp => () => sp.GetRequiredService<IApiRequestBuilder>()); 

A few rules

  • Validate in Build(), and throw a clear message for every missing or invalid part.
  • Build immutable results: records, init, required, and copies of the builder's collections.
  • Don't build what an initializer can build: new Point(10, 20) doesn't need a builder.
  • Keep the builder short-lived: create it, configure it, build, throw it away.

Want the full story?

This is the short version. The complete article on Level Up Coding follows the Builder from the GoF Director to today: the fluent revolution, records and init properties, builders with Dependency Injection, asynchronous builders for the cloud, builders generated with Source Generators, pooling and ref struct builders for hot paths, a complete production-style implementation, and how to test builders.

Read it here: The Builder Pattern in .NET C#: From Complex Constructors to Fluent APIs.

Thanks for reading.