Table of Contents
- 2.1 Design pattern categories
- 2.2 Formal definition
- 2.3 UML Diagram
- 2.4 When to use the Builder
- 3.1 Project structure
- 3.2 The FurnitureItem class (data object)
- 3.3 The InventoryReport class (complex object)
- 3.4 The Main Program (Starter)
- 5.1 DailyReportBuilder structure
- 5.2 The Reset method and the constructor
- 5.3 Implementation of construction methods
- 5.4 Finished product recovery — GetDailyReport
- 7.1 Fluent Builder Concept
- 7.2 Changing the interface for chaining
- 7.3 Concrete Builder Update
- 7.4 Using Fluent Builder in hand
- 9.1 When to avoid the Builder
- 9.2 Good application scenarios
- 9.3 Key implications of the Builder pattern
- 9.4 Builder vs Factory Pattern
1. Course Overview
This course is presented by Harrison Ferrone, software developer and educational content author at Paradigm Shift Development. He works primarily with C# in Unity, does freelance iOS development, and edits technical content for the Ray Wenderlich website.
Design patterns have been around for over 25 years and have helped countless developers solve complex problems using proven techniques.
Course objectives
This course is a starting point for the Builder design pattern in C#. No prior experience with design patterns is required. The main themes covered are:
- Define an object class
- Add a Builder interface
- Create a Concrete Builder class
- Implement a Director
- Understanding real-world examples and practical implications
Prerequisites
This is an intermediate level course. You should be familiar with:
- The C# programming language
- Visual Studio (on Mac or Windows)
2. The Builder design pattern explained
2.1 Categories of design patterns
Design patterns are classified into three categories, as defined in the famous work of the Gang of Four (Elements of Reusable Object-Oriented Software):
| Category | Description |
|---|---|
| Creational (Creation) | Patterns related to object creation |
| Structural (Structure) | Patterns related to the composition of objects and classes |
| Behavioral | Patterns related to algorithms and communication between objects |
The Builder Pattern belongs to the Creational category, because it deals with the creation of objects.
2.2 Formal definition
“Separate the construction of a complex object from its representation so that the same construction process can create different representations.” — Gang of Four
In more accessible terms: the Builder pattern removes all the construction or initialization code from an object class and abstracts it into an interface. The specific representations of this base class are then created as concrete classes implementing this interface, building themselves from the blueprint provided.
The interesting part of the process is that concrete classes do not manage their own instantiation — that is the role of the Director class, which controls where and with what data the concrete classes are actually created.
2.3 UML Diagram
┌──────────────┐ ┌─────────────────────┐ ┌─────────────┐
│ Director │─────────►│ <<interface>> │ │ Product │
│ │ │ Builder │ │ (Complex │
│ + Construct()│ │ + BuildPartA() │ │ Object) │
└──────────────┘ │ + BuildPartB() │ └─────────────┘
│ + BuildPartC() │ ▲
└──────────────────────┘ │
▲ │
│ implements │
┌─────────────────────┐ │
│ ConcreteBuilder │─────────────────►
│ + BuildPartA() │ creates
│ + BuildPartB() │
│ + BuildPartC() │
│ + GetResult() │
└─────────────────────┘
- Product (right): the complex object to construct — it contains no initialization logic.
- Builder (interface): defines how the parts of the complex object are created.
- ConcreteBuilder: builds the complex object according to the plan of the Builder interface. Each Concrete Builder is responsible for following the representation of the object it creates and returning it when requested.
- Director: handles the actual call to build the complex object using the Concrete Builder.
2.4 When to use the Builder
Warning: The Builder Pattern is not very common in production code, because its suitable usage scenarios are not very common.
The Builder Pattern is useful when:
- The creation of a complex object must be separated from its parts and their assembly.
- Different representations of the same object must be able to be created.
- Finer control over how the object is assembled is needed.
3. Startup Project — Initial Code
3.1 Project structure
The Starter project is a .NET Core 3.1 console application with two main files:
Builder Pattern.sln
└── Builder Pattern/
├── Builder Pattern.csproj
├── Program.cs
└── InventoryReport.cs
Project file (.csproj):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Builder_Pattern</RootNamespace>
</PropertyGroup>
</Project>
3.2 The FurnitureItem class (data object)
FurnitureItem is a simple data object representing a furniture item, with several public fields and a constructor:
public class FurnitureItem
{
public string Name;
public double Price;
public double Height;
public double Width;
public double Weight;
public FurnitureItem(string productName, double price, double height, double width, double weight)
{
this.Name = productName;
this.Price = price;
this.Height = height;
this.Width = width;
this.Weight = weight;
}
}
3.3 The InventoryReport class (complex object)
InventoryReport is the complex object that we will build with the Builder pattern. It has three string fields which represent the sections of the report, and a Debug() method to display the result in the console.
Important note: There is no constructor in this class, because the values will be defined one by one in the Concrete Builder representations.
public class InventoryReport
{
public string TitleSection;
public string DimensionsSection;
public string LogisticsSection;
public string Debug()
{
return new StringBuilder()
.AppendLine(TitleSection)
.AppendLine(DimensionsSection)
.AppendLine(LogisticsSection)
.ToString();
}
}
The Debug() method already illustrates a common use of the Fluent Builder with StringBuilder: we initialize an instance, then we chain configuration calls (AppendLine), and finally we return the complete result with ToString().
3.4 The Main Program (Starter)
In the startup version, Program.cs only contains the list of furniture items already prepared:
using System;
using System.Collections.Generic;
namespace Builder_Pattern
{
class Program
{
static void Main(string[] args)
{
var items = new List<FurnitureItem>
{
new FurnitureItem("Sectional Couch", 55.5, 22.4, 78.6, 35.0),
new FurnitureItem("Nightstand", 25.0, 12.4, 20.0, 10.0),
new FurnitureItem("Dining Table", 105.0, 35.4, 100.6, 55.5),
};
}
}
}
4. Create a Builder interface
4.1 Why an interface
The Builder interface draws the plan (blueprint) for the construction of each section of the report. It should be general enough to apply to all types of Concrete Builder classes that might be created in the future.
In our example, the same interface could be applied to classes generating monthly, quarterly or annual reports without any problems.
Key benefit: the interface effectively breaks down the product build into individual steps. This provides increased control over how an object is assembled, and opens possibilities for error handling and sequential calculations at each step.
4.2 Declaration of IFurnitureInventoryBuilder
The interface is declared under the InventoryReport class in the InventoryReport.cs file:
public interface IFurnitureInventoryBuilder
{
IFurnitureInventoryBuilder AddTitle();
IFurnitureInventoryBuilder AddDimensions();
IFurnitureInventoryBuilder AddLogistics(DateTime dateTime);
InventoryReport GetDailyReport();
}
Important points:
- The
AddTitle,AddDimensionsandAddLogisticsmethods correspond to the three sections of the report. - The
GetDailyReport()method returns theInventoryReportobject when constructed. In the traditional pattern, each Concrete Builder implements its own return method. Here, since the case is sufficiently generic, we place it directly in the interface for greater clarity. - If your scenario requires a less generic implementation, leave
GetDailyReport()out of the interface — but definitely add it in each concrete class. AddLogisticstakes aDateTimeparameter: this illustrates how to pass external data to interface methods.
5. Implement a Concrete Builder
5.1 Structure of DailyReportBuilder
The DailyReportBuilder class implements IFurnitureInventoryBuilder and represents the daily inventory report. It is responsible for tracking and configuring an InventoryReport object.
5.2 The Reset method and the constructor
To keep report tracking clean and secure, we declare a Reset() method which ensures that the report is always instantiated with a new InventoryReport object.
public class DailyReportBuilder : IFurnitureInventoryBuilder
{
private InventoryReport _report;
private IEnumerable<FurnitureItem> _items;
public DailyReportBuilder(IEnumerable<FurnitureItem> items)
{
Reset();
_items = items;
}
public void Reset()
{
_report = new InventoryReport();
}
// ...
}
Why Reset()? This is essential defensive programming in this scenario. Calling Reset() in the constructor ensures that a blank product object accompanies each new instance of the Concrete Builder.
Two ways to provide external data to Concrete Builder:
- By the constructor: we pass
IEnumerable<FurnitureItem>to the constructor (used forAddDimensions). - By method parameters: we pass a
DateTimedirectly toAddLogistics(used for the report date).
5.3 Implementation of construction methods
AddTitle — hardcoded data
public void AddTitle()
{
_report.TitleSection = "------- Daily Inventory Report -------\n\n";
}
In simple examples, the data is often hardcoded directly into the method. In most real-world cases you will need data from outside.
AddDimensions — manufacturer data
We use the data passed via the constructor (_items) with LINQ to format the list of items:
public void AddDimensions()
{
_report.DimensionsSection = string.Join(Environment.NewLine, _items.Select(product =>
$"Product: {product.Name} \nPrice: {product.Price} \n" +
$"Height: {product.Height} x Width: {product.Width} -> Weight: {product.Weight} lbs \n"));
}
Don’t forget to add
using System.Linq;at the top of the file to be able to use.Select().
AddLogistics — method parameter
public void AddLogistics(DateTime dateTime)
{
_report.LogisticsSection = $"Report generated on {dateTime}";
}
Important Note: The order in which the build steps are executed is entirely within your control. You don’t have to call the interface’s methods in the order they are declared. Keep this in mind when data or calculations need to be executed in a specific order.
5.4 Finished product recovery — GetDailyReport
public InventoryReport GetDailyReport()
{
InventoryReport finishedReport = _report;
Reset();
return finishedReport;
}
When returning the built report to the calling code, the Concrete Builder is immediately reset to begin building a new report. This is not required, but it is a good practice: it ensures that the instance is ready for a future build.
6. Add a Director class
6.1 Role of the Director
The Director class has one role: to execute the object construction steps in a predetermined sequence. Directors will be found in some Builder implementations and not in others, because it is technically possible to call construction steps directly from any Concrete Builder class. However, using a Director allows for a cleaner abstraction.
6.2 Declaration of InventoryBuildDirector
public class InventoryBuildDirector
{
private IFurnitureInventoryBuilder _builder;
public InventoryBuildDirector(IFurnitureInventoryBuilder concreteBuilder)
{
_builder = concreteBuilder;
}
public void BuildCompleteReport()
{
_builder.AddTitle();
_builder.AddDimensions();
_builder.AddLogistics(DateTime.Now);
}
}
Important points:
- The Director stores a reference to the
IFurnitureInventoryBuilderinterface, not a concrete class. - The Concrete Builder is injected into the Director constructor (dependency injection).
- You can declare as many public methods as necessary to build different versions of a product.
- If you construct a partial object (without calling all steps), uninitialized values will be set to their default value. This can cause errors if you use lists or other types that require initialization.
6.3 Use in the Main Program
// Créer une instance du Concrete Builder en lui passant la liste d'articles
var inventoryBuilder = new DailyReportBuilder(items);
// Créer une instance du Director en lui fournissant le Concrete Builder
var director = new InventoryBuildDirector(inventoryBuilder);
// Le Director construit le rapport selon la séquence prédéfinie
director.BuildCompleteReport();
// Récupérer le rapport depuis le Concrete Builder et l'afficher
var directorReport = inventoryBuilder.GetDailyReport();
Console.WriteLine(directorReport.Debug());
Expected console output:
------- Daily Inventory Report -------
Product: Sectional Couch
Price: 55.5
Height: 22.4 x Width: 78.6 -> Weight: 35 lbs
Product: Nightstand
Price: 25
Height: 12.4 x Width: 20 -> Weight: 10 lbs
Product: Dining Table
Price: 105
Height: 35.4 x Width: 100.6 -> Weight: 55.5 lbs
Report generated on [date et heure actuelles]
7. Fluent Builder variant
7.1 Fluent Builder Concept
The Fluent Builder is a popular variation of the Builder pattern. If you look at the Debug() method in InventoryReport, you will already see an example of Fluent Builder with StringBuilder: after initialization, you can chain as many configurations as you want with methods like AppendLine, then return everything with ToString().
This variant is often chosen for its intuitive syntax, which allows you to initialize a Concrete Builder instance, chain construction steps, and finally return the object — all in a single expression.
7.2 Modification of the interface for chaining
To enable method chaining, each build method must return an object of its own type (here, IFurnitureInventoryBuilder). Only GetDailyReport() remains unchanged since it returns the finished product.
public interface IFurnitureInventoryBuilder
{
IFurnitureInventoryBuilder AddTitle();
IFurnitureInventoryBuilder AddDimensions();
IFurnitureInventoryBuilder AddLogistics(DateTime dateTime);
InventoryReport GetDailyReport();
}
(Return signatures changed from void to IFurnitureInventoryBuilder.)
7.3 Concrete Builder Update
Each construction method must now return this (the current instance):
public IFurnitureInventoryBuilder AddTitle()
{
_report.TitleSection = "------- Daily Inventory Report -------\n\n";
return this;
}
public IFurnitureInventoryBuilder AddDimensions()
{
_report.DimensionsSection = string.Join(Environment.NewLine, _items.Select(product =>
$"Product: {product.Name} \nPrice: {product.Price} \n" +
$"Height: {product.Height} x Width: {product.Width} -> Weight: {product.Weight} lbs \n"));
return this;
}
public IFurnitureInventoryBuilder AddLogistics(DateTime dateTime)
{
_report.LogisticsSection = $"Report generated on {dateTime}";
return this;
}
The Director class does not need to be modified, because it can still call the methods separately — it will just ignore the return value.
7.4 Using Fluent Builder in hand
The Fluent Builder variant is usually called directly from the Concrete Builder, completely bypassing the Director. The construction steps and returning the object all occur in the same line:
var fluentReport = inventoryBuilder
.AddTitle()
.AddDimensions()
.AddLogistics(DateTime.Now)
.GetDailyReport();
Console.WriteLine(fluentReport.Debug());
Result: identical to the approach with Director.
It would technically be possible to create a method in the Director that uses Fluent Builder syntax, but that represents a lot of abstraction for very little added value. As with all design patterns, always choose the implementation that offers the best quality/complexity ratio for your case, without adding unnecessary infrastructure.
8. Complete final code
8.1 InventoryReport.cs — final version
This file contains all the classes, the interface and the Director:
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace Builder_Pattern
{
// Objet de données simple
public class FurnitureItem
{
public string Name;
public double Price;
public double Height;
public double Width;
public double Weight;
public FurnitureItem(string productName, double price, double height, double width, double weight)
{
this.Name = productName;
this.Price = price;
this.Height = height;
this.Width = width;
this.Weight = weight;
}
}
// Objet complexe (Product) — sans constructeur, car Builder gère l'initialisation
public class InventoryReport
{
public string TitleSection;
public string DimensionsSection;
public string LogisticsSection;
public string Debug()
{
return new StringBuilder()
.AppendLine(TitleSection)
.AppendLine(DimensionsSection)
.AppendLine(LogisticsSection)
.ToString();
}
}
// Interface Builder (blueprint de construction)
public interface IFurnitureInventoryBuilder
{
IFurnitureInventoryBuilder AddTitle();
IFurnitureInventoryBuilder AddDimensions();
IFurnitureInventoryBuilder AddLogistics(DateTime dateTime);
InventoryReport GetDailyReport();
}
// Concrete Builder — représentation d'un rapport quotidien
public class DailyReportBuilder : IFurnitureInventoryBuilder
{
private InventoryReport _report;
private IEnumerable<FurnitureItem> _items;
public DailyReportBuilder(IEnumerable<FurnitureItem> items)
{
Reset();
_items = items;
}
public IFurnitureInventoryBuilder AddTitle()
{
_report.TitleSection = "------- Daily Inventory Report -------\n\n";
return this;
}
public IFurnitureInventoryBuilder AddDimensions()
{
_report.DimensionsSection = string.Join(Environment.NewLine, _items.Select(product =>
$"Product: {product.Name} \nPrice: {product.Price} \n" +
$"Height: {product.Height} x Width: {product.Width} -> Weight: {product.Weight} lbs \n"));
return this;
}
public IFurnitureInventoryBuilder AddLogistics(DateTime dateTime)
{
_report.LogisticsSection = $"Report generated on {dateTime}";
return this;
}
public InventoryReport GetDailyReport()
{
InventoryReport finishedReport = _report;
Reset();
return finishedReport;
}
public void Reset()
{
_report = new InventoryReport();
}
}
// Director — orchestre les étapes de construction dans une séquence prédéfinie
public class InventoryBuildDirector
{
private IFurnitureInventoryBuilder _builder;
public InventoryBuildDirector(IFurnitureInventoryBuilder concreteBuilder)
{
_builder = concreteBuilder;
}
public void BuildCompleteReport()
{
_builder.AddTitle();
_builder.AddDimensions();
_builder.AddLogistics(DateTime.Now);
}
}
}
8.2 Program.cs — final version
using System;
using System.Collections.Generic;
namespace Builder_Pattern
{
class Program
{
static void Main(string[] args)
{
// Données d'entrée
var items = new List<FurnitureItem>
{
new FurnitureItem("Sectional Couch", 55.5, 22.4, 78.6, 35.0),
new FurnitureItem("Nightstand", 25.0, 12.4, 20.0, 10.0),
new FurnitureItem("Dining Table", 105.0, 35.4, 100.6, 55.5),
};
// Instanciation du Concrete Builder
var inventoryBuilder = new DailyReportBuilder(items);
// --- Option 1 : Utilisation avec le Director ---
/*
var director = new InventoryBuildDirector(inventoryBuilder);
director.BuildCompleteReport();
var directorReport = inventoryBuilder.GetDailyReport();
Console.WriteLine(directorReport.Debug());
*/
// --- Option 2 : Utilisation avec le Fluent Builder (sans Director) ---
var fluentReport = inventoryBuilder
.AddTitle()
.AddDimensions()
.AddLogistics(DateTime.Now)
.GetDailyReport();
Console.WriteLine(fluentReport.Debug());
}
}
}
9. Use cases and implications
9.1 When to avoid Builder
The Builder pattern is excessive (overkill) for most classes where these solutions would be more appropriate:
- Subclassing
- Refactoring
- Abstract interfaces or classes
9.2 Good application scenarios
Signal 1 — Bloated Constructor: If you have a class whose constructor continually expands and performs a variety of calculations before setting the class fields, this is a good indication that the Builder Pattern may be useful.
Signal 2 — Finite number of related classes: If you have a finite number of related classes that perform the same general function but with different representations, the Builder Pattern is also indicated.
Worked example — text parsing: A text parsing scenario where you need to handle general functionality but accommodate different types of text input and output (as described in the Gang of Four text). This is an excellent scenario for the Builder because there are a finite number of complex representations that must share similar object construction sequences.
9.3 Key implications of the Builder pattern
1. Variation of internal product representation
The Director does not need to know how an object is constructed or its internal structure. Changing the internal structure of a product is as simple as creating a new Concrete Builder class.
2. Construction and representation code isolation
The code is more modular and encapsulated. The object creation code is written once in the Concrete Builders, then the Director only has to choose which predefined construction sequence to use and reuse.
3. Finer control over the build process
The Director has full control over each construction step and its sequence. The returned object is only available when the Director has finished calling the construction sequence — this is a major advantage for the error checking and calculations involved in creating a complex object.
9.4 Builder vs Factory Pattern
If you have experience with other design patterns, you have probably noticed that the Builder shares similarities with the Factory Pattern. The important difference to keep in mind:
| Boss | Focus |
|---|---|
| Builder | Creating objects in sequential steps |
| Factory | Creation of families or groups of objects |
10. Summary and conclusion
What we learned
Throughout this course we have covered:
-
The place of the Builder in the panorama of design patterns: Creational category, inspired by the Gang of Four.
-
Create an object class and abstract its constructor into an interface: the
InventoryReportclass without constructor, and theIFurnitureInventoryBuilderinterface which defines the construction steps. -
Create and configure Concrete Builder representations: the
DailyReportBuilderclass with itsReset()method, its constructor, and the implementation of all the interface methods. -
The role of the Director class:
InventoryBuildDirectorwhich orchestrates the overall build process by calling the steps in a predefined order. -
Pattern variations with dependency injection: passing data via the constructor or via method parameters.
-
The Fluent Builder variant: return
thisin each method to allow chaining of calls in a single expression. -
The most common use cases and implications: when to apply the pattern, when to avoid it, and how it differs from the Factory Pattern.
Summary architecture
IFurnitureInventoryBuilder (Interface)
|
|-- DailyReportBuilder (Concrete Builder)
| |- AddTitle() → configure TitleSection
| |- AddDimensions() → configure DimensionsSection (via constructor data)
| |- AddLogistics() → configure LogisticsSection (via method parameter)
| |- GetDailyReport() → retourne InventoryReport + Reset()
| └- Reset() → réinitialise _report à new InventoryReport()
|
└-- InventoryBuildDirector (Director)
└- BuildCompleteReport() → appelle AddTitle + AddDimensions + AddLogistics
Resources to go further
- Other C# design pattern courses available in the Pluralsight library
- Design Patterns: Elements of Reusable Object-Oriented Software — Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)
Search Terms
c-sharp · design · patterns · builder · testing · architecture · c# · .net · development · class · data · fluent · interface · pattern · concrete · construction · declaration · director · implications · method · object · product · program · representation