Intermediate

C-Sharp Design Patterns Builder

He works primarily with C# in Unity, does freelance iOS development, and edits technical content for the Ray Wenderlich website.

Table of Contents

  1. Course Overview
  2. The Builder design pattern explained
  1. Startup Project — Initial Code
  1. Create a Builder interface
  1. Implement a Concrete Builder
  1. Add a Director class
  1. Fluent Builder variant
  1. Complete final code
  1. Use cases and implications
  1. Summary and conclusion

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):

CategoryDescription
Creational (Creation)Patterns related to object creation
Structural (Structure)Patterns related to the composition of objects and classes
BehavioralPatterns 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, AddDimensions and AddLogistics methods correspond to the three sections of the report.
  • The GetDailyReport() method returns the InventoryReport object 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.
  • AddLogistics takes a DateTime parameter: 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:

  1. By the constructor: we pass IEnumerable<FurnitureItem> to the constructor (used for AddDimensions).
  2. By method parameters: we pass a DateTime directly to AddLogistics (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 IFurnitureInventoryBuilder interface, 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:

BossFocus
BuilderCreating objects in sequential steps
FactoryCreation of families or groups of objects

10. Summary and conclusion

What we learned

Throughout this course we have covered:

  1. The place of the Builder in the panorama of design patterns: Creational category, inspired by the Gang of Four.

  2. Create an object class and abstract its constructor into an interface: the InventoryReport class without constructor, and the IFurnitureInventoryBuilder interface which defines the construction steps.

  3. Create and configure Concrete Builder representations: the DailyReportBuilder class with its Reset() method, its constructor, and the implementation of all the interface methods.

  4. The role of the Director class: InventoryBuildDirector which orchestrates the overall build process by calling the steps in a predefined order.

  5. Pattern variations with dependency injection: passing data via the constructor or via method parameters.

  6. The Fluent Builder variant: return this in each method to allow chaining of calls in a single expression.

  7. 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.