You already use a real Builder every time you write WebApplication.CreateBuilder(args) — this lesson gives that shape its name.
Imagine a Report class with a title, an author, an optional date range, an optional list of chart types, an optional footer note, and a flag for whether to include raw data tables. A constructor covering every combination looks like new Report("Q3 Sales", "J. Smith", start, end, chartTypes, footer, true) — seven positional arguments, most of them optional, and utterly unreadable at the call site. Add an eighth optional field next quarter, and every single call site needs to change, or you write a second, third, and fourth overload just to cover the common combinations. This is the classic telescoping constructor problem: constructors that grow longer and more overloaded every time a new optional piece of configuration is needed.
The Builder Pattern — a Creational pattern, alongside Factory (240) — solves this by separating how an object gets constructed, step by step from what the finished object looks like. Instead of one enormous constructor call, you set pieces one at a time, in readable, named steps, and ask for the finished object only once every piece is in place.
In this lesson, you'll see the telescoping constructor problem Builder was designed to solve — and, just as importantly, you'll see honestly that modern C# already solves a large part of that same problem without a Builder at all. Then you'll learn precisely when a real Builder still earns its keep, and build one from scratch.
A Builder is a separate object whose entire job is assembling another object piece by piece, through a sequence of clearly named steps, and handing back the finished result only when you explicitly ask for it — usually by calling a final .Build() method.
The GoF definition: separate the construction of a complex object from its representation, so that the same construction process can create different representations. In practice, that means the object being built (often called the "product") doesn't know how it's being assembled — the Builder owns the assembly sequence, can validate or apply logic at each step, and only produces the finished, ready-to-use object at the very end.
Factory (240) answers "which concrete class should get instantiated?" Builder answers a completely different question: "this one object is complicated enough that constructing it needs multiple, separate steps — how do I make that process clean instead of one enormous constructor call?" Both are Creational — both are about controlling object construction — but Factory centralizes a decision, while Builder centralizes a process.
// THE TELESCOPING CONSTRUCTOR PROBLEM — one overload per "reasonable" combination
public class Report
{
public Report(string title, string author) { /* ... */ }
public Report(string title, string author, DateTime start, DateTime end) { /* ... */ }
public Report(string title, string author, DateTime start, DateTime end, bool includeCharts) { /* ... */ }
public Report(string title, string author, DateTime start, DateTime end, bool includeCharts, string footer) { /* ... */ }
// ...and every NEW optional field means another overload, or editing an existing one
// and hunting down every call site that used it.
}
// At the call site — which bool is which? Which overload is even being called here?
var report = new Report("Q3 Sales", "J. Smith", start, end, true, "Confidential");
.Build() callHere's the important, honest twist this lesson has to address directly: in 1994, when the GoF catalog was written, C# didn't exist, and even early C# had no clean way to set several optional properties readably in one expression. That's completely changed. Object initializers, required members (Foundations lesson 057), and primary constructors (Foundations lesson 058) together solve the "object has many optional properties" problem directly, with zero extra classes:
| Tool | What it solves | Lesson |
|---|---|---|
| Object initializer syntax | Set only the properties you care about, by name, in any order — no positional confusion | Foundations, early OOP lessons |
required members | The compiler enforces that specific properties MUST be set, even inside a flexible object initializer | 057 |
| Primary constructors | A concise way to require a few genuinely mandatory values up front, while everything else stays initializer-driven | 058 |
// MODERN C# — no Builder class needed for "many optional properties"
public class Report
{
public required string Title { get; init; }
public required string Author { get; init; }
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
public bool IncludeCharts { get; init; } = false;
public string? Footer { get; init; }
}
// Readable, named, order-independent, and the COMPILER enforces Title and Author:
var report = new Report
{
Title = "Q3 Sales",
Author = "J. Smith",
IncludeCharts = true,
Footer = "Confidential"
};
// var broken = new Report { Author = "J. Smith" }; // compile error — missing required 'Title'
required plus primary constructors already solve that problem, with less code than a hand-written Builder, and no extra class to maintain. A real Builder earns its place for a genuinely different reason — covered next.
Object initializers set properties on an object that already, technically, exists as a single expression. A real Builder is warranted when construction is genuinely a process — something that happens across multiple, distinct steps, not a single flat list of property values:
WebApplicationBuilder and HostApplicationBuilder (Intermediate lessons 124 and 238) are genuine, production Builder pattern implementations. var builder = WebApplication.CreateBuilder(args); creates the builder; builder.Services.AddScoped<...>() and builder.Configuration.AddJsonFile(...) are fluent step-by-step configuration calls, each one mutating the builder's internal state; and var app = builder.Build(); is the exact same "finalize and produce the product" step every Builder ends with. This isn't a coincidence or a loose analogy — it is, structurally, the Builder pattern, built into the ASP.NET Core hosting model you've been using since the Intermediate book.
Report — the finished, immutable object the builder ultimately hands backReportBuilder — private, mutable fields collecting each piece as it's addedthis, ENABLING FLUENT CHAININGWithTitle(...), WithAuthor(...), AddChart(...) — each one mutates internal state and returns the builder itself, so calls can be chained one after another.Build() VALIDATES AND PRODUCES THE FINAL, IMMUTABLE OBJECTReport constructed — this is also the natural place to enforce "mandatory pieces were actually set" before handing back a usable object.public sealed class ReportBuilder
{
private string? _title;
private string? _author;
private readonly List<string> _sections = [];
public ReportBuilder WithTitle(string title)
{
_title = title;
return this; // ← enables fluent chaining
}
public ReportBuilder WithAuthor(string author)
{
_author = author;
return this;
}
public ReportBuilder AddSection(string section)
{
_sections.Add(section);
return this;
}
public Report Build()
{
if (string.IsNullOrWhiteSpace(_title))
throw new InvalidOperationException("A report must have a title before it can be built.");
return new Report(_title, _author ?? "Unknown", _sections.AsReadOnly());
}
}
// ── Usage — a readable, step-by-step, fluent chain ──
var report = new ReportBuilder()
.WithTitle("Q3 Sales")
.WithAuthor("J. Smith")
.AddSection("Revenue")
.AddSection("Regional Breakdown")
.Build();
Code → Meaning → Result: Each fluent call returns the builder itself, so calls chain naturally. Build() is the single point where validation happens (a report needs a title) and where the actual, immutable Report gets constructed. Nothing about ReportBuilder's shape would fit cleanly into a single object initializer, because sections accumulate through repeated calls to AddSection — a genuine multi-step process, not a flat list of properties.
A query builder for a search feature is a case where each step genuinely needs to validate as it goes, and the final shape of the query depends on which steps were called at all:
public sealed record ProductQuery(string? Keyword, decimal? MinPrice, decimal? MaxPrice, string? Category, int PageSize);
public sealed class ProductQueryBuilder
{
private string? _keyword;
private decimal? _minPrice;
private decimal? _maxPrice;
private string? _category;
private int _pageSize = 20;
public ProductQueryBuilder WithKeyword(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
throw new ArgumentException("Keyword cannot be blank.", nameof(keyword));
_keyword = keyword;
return this;
}
public ProductQueryBuilder WithPriceRange(decimal min, decimal max)
{
// ── Validation happens progressively, AS each piece is added — not deferred to the end ──
if (min < 0 || max < min)
throw new ArgumentException("Invalid price range.");
_minPrice = min;
_maxPrice = max;
return this;
}
public ProductQueryBuilder InCategory(string category)
{
_category = category;
return this;
}
public ProductQueryBuilder PageSize(int size)
{
if (size is < 1 or > 100)
throw new ArgumentOutOfRangeException(nameof(size), "Page size must be between 1 and 100.");
_pageSize = size;
return this;
}
public ProductQuery Build() => new(_keyword, _minPrice, _maxPrice, _category, _pageSize);
}
// ── Usage — steps applied conditionally, based on which search filters the user actually chose ──
var builder = new ProductQueryBuilder().PageSize(50);
if (!string.IsNullOrEmpty(searchTerm)) builder.WithKeyword(searchTerm);
if (selectedCategory is not null) builder.InCategory(selectedCategory);
if (minPrice.HasValue && maxPrice.HasValue) builder.WithPriceRange(minPrice.Value, maxPrice.Value);
var query = builder.Build();
Notice what this buys you that an object initializer couldn't: WithPriceRange validates min and max together, at the moment they're added — a rule an object initializer's independent property assignments can't express. And the calling code conditionally invokes only the steps it needs, based on which filters the user actually selected — a genuinely step-based, branching construction process.
Filling out a form with checkboxes for "extra cheese" and "no onions" is like an object initializer — you specify everything you want, all at once, in one flat submission. But a made-to-order sandwich shop works differently: you choose the bread first, and that choice affects what's offered next; you add ingredients one at a time, in a sequence, and the person behind the counter can catch a problem as it happens ("we're out of that cheese — want to pick something else before I add the rest?"). Only when you say "that's everything" does the sandwich actually get wrapped and handed to you.
A Builder is the sandwich line, not the checkbox form: a sequence of steps, each one able to depend on or validate against what came before, producing the finished result only when you explicitly finish the process. Reach for a Builder specifically when your object's construction genuinely resembles the sandwich line — not when it's really just a form with a lot of optional fields.
this IS WHAT MAKES CHAINING POSSIBLE — NOTHING MORE MAGICAL THAN THAT.WithTitle(...).WithAuthor(...).Build() reading as one continuous expression — no special C# syntax is involved, just an ordinary method return value.ReportBuilder's private fields change with every step call, but Report itself — once Build() returns it — is typically immutable, often a record, with no way to mutate it after construction. The mutability is contained entirely inside the temporary builder; the final object stays safe to share and reason about.WithPriceRange checking min < max immediately) catches a problem the instant it's introduced. Final validation in Build() (like requiring a title) catches missing pieces only detectable once every step is done. A well-designed Builder uses both, deliberately, rather than deferring everything to the end.No — and the distinction is exactly what this lesson is built around. An object initializer sets properties on an object in one single expression; there's no accumulated state between calls, no way for one property's value to be validated against another's as they're set, and no notion of "steps" happening in sequence at all — it's syntactic sugar over setting properties, not a process. A real Builder maintains its own mutable state across multiple, separate method calls, and can enforce relationships between pieces as they're added. If your "construction" genuinely fits into one flat expression, you don't have a Builder problem — you have an object-initializer-shaped problem, and 057/058 already solve it.
Factory (240) answers "which concrete class should be instantiated?" — a decision, usually made instantly, based on some input. Builder answers "how do I assemble this one, already-known type across several steps?" — a process, potentially spanning many method calls over time. You can genuinely combine them — a Factory could internally use a Builder to assemble the object it returns — but they solve different-shaped problems and shouldn't be confused for the same thing just because both are Creational.
Hand-rolling a PersonBuilder with .WithName(...), .WithAge(...), .WithEmail(...) for a flat data object with no cross-field validation and no real construction sequence — pure ceremony around what an object initializer already does more simply. Reach for object initializers plus required (057) first; only introduce a Builder once genuine multi-step logic or progressive validation is actually present.
Build(), letting an incomplete object escape A ReportBuilder.Build() that happily returns a Report with no title set, silently, because no check was ever added — the whole point of centralizing construction is lost if the final step doesn't actually enforce the invariants it's supposed to guard. Treat Build() as the one place that must validate every mandatory piece is present before handing back a usable object — exactly as shown in this lesson's examples.
Having Build() return an object with public setters, so code elsewhere can silently mutate it after construction, bypassing every validation rule the Builder carefully enforced. Make the finished product immutable — a record with init-only properties, or a class with a private constructor only the Builder can call — so the guarantees established during building actually hold for the object's entire lifetime.
WebApplicationBuilder itself — where chained, named method calls read more clearly than a giant constructor or options object.And when it's overkill: if the "problem" is really just "this object has several optional properties," reach for object initializers, required members (057), and primary constructors (058) first — they solve exactly that problem, with less code, no extra class, and no risk of an incomplete object escaping through a forgotten step. A Builder is warranted by genuine process, not merely by property count.
new Report { ... } expression with no cross-field logic, you don't need a Builder. If assembly genuinely spans multiple steps, conditionally, with validation along the way, a Builder is exactly the right tool — and .NET's own WebApplicationBuilder is proof it scales to real, production use.
WebApplicationBuilder/HostApplicationBuilder is a real, production Builder you've already used since Intermediate 124/238.this; the finished product should be immutable.
required members (057), primary constructors (058) — already solves "many optional properties," without needing a Builder class at all.WebApplicationBuilder, a real, production .NET Builder you've already used.this to enable chaining; .Build() is where final validation happens and the immutable, finished product gets constructed.You've seen when modern C# already solves the problem Builder was invented for, and when a real Builder still earns its keep. Let's confirm you can tell the two situations apart.
1. A Customer class has six optional properties, no cross-field validation, and is always fully known at the moment it's created. Which approach does this lesson recommend?
Correct: B
Why B is correct: With no cross-field validation and no multi-step process, this is exactly the "many optional properties" problem that object initializers plus required already solve — with less code and no extra class.
Why A is incorrect: A Builder is warranted by genuine process or progressive validation, neither of which is present here — this would be Builder ceremony around a problem that doesn't need it.
Why C is incorrect: This is exactly the telescoping constructor problem the lesson opens with — the source of unreadable, hard-to-maintain call sites.
Why D is incorrect: Factory Method addresses which concrete type gets constructed via inheritance — an unrelated problem to setting properties on one known type.
Reinforcement: "Many optional properties, no process, no cross-field logic" is an object-initializer-shaped problem, not a Builder-shaped one.
2. Why is WebApplicationBuilder described in this lesson as a genuine, real-world implementation of the Builder pattern?
Correct: B
Why B is correct: The defining structural trait of a Builder — accumulating state through step-by-step calls, then producing the finished product through one explicit finalizing call — is exactly what builder.Services.AddScoped(...) followed by builder.Build() does.
Why A is incorrect: A class's name alone doesn't establish a pattern — the lesson identifies it by its actual structural shape, not by naming convention.
Why C is incorrect: A switch deciding which concrete type to construct describes Factory (240), not Builder — this lesson is specifically about assembling ONE known type across steps.
Why D is incorrect: Inheriting from a specific base class isn't required by, or even typical of, the Builder pattern — the pattern is about structural behavior, not a shared base type.
Reinforcement: A pattern is identified by its structural shape and behavior, not by a class name or inheritance relationship.
3. In the ProductQueryBuilder example, why does WithPriceRange(min, max) validate min < max immediately, inside the step method itself, rather than deferring that check to Build()?
Correct: B
Why B is correct: The rule "min must be less than max" only makes sense to check once both values are known together — checking it the instant they're both supplied catches the problem as early as possible, which is precisely the progressive-validation capability a flat object initializer doesn't have.
Why A is incorrect: Nothing in C# restricts where validation logic can live — this is a design choice about when validation is most useful, not a language constraint.
Why C is incorrect: Build() methods throw exceptions constantly in real Builder implementations, including this lesson's own ReportBuilder.Build() example.
Why D is incorrect: Catching the problem immediately, at the step where the bad data was introduced, gives more useful, immediate feedback than discovering it only after several more unrelated steps have already run.
Reinforcement: Progressive, per-step validation is one of the genuine advantages a Builder offers over a single flat object initializer.
4. What is the key difference between the Builder pattern and the Factory pattern (240), even though both are Creational?
Correct: B
Why B is correct: Factory centralizes a decision about which concrete type to construct; Builder centralizes the process of assembling a single, already-known type through multiple steps — genuinely different problems, even though both fall under the Creational category.
Why A is incorrect: The lesson explicitly distinguishes them by the shape of problem each one solves — treating them as identical loses that distinction entirely.
Why C is incorrect: Neither pattern is tied to a specific application type — both are general object-oriented design shapes usable anywhere in C#.
Why D is incorrect: Builder is explicitly a Creational pattern, alongside Factory — both concern object construction, just in different ways.
Reinforcement: "Which type?" is Factory's question; "how do I assemble this one type, across steps?" is Builder's question.
You now know exactly when modern C# already solves your construction problem, and when a real Builder — the same shape as WebApplicationBuilder itself — genuinely earns its place.
dotnetmadeeasy.com — Learn C# and .NET, the right way.