Skip to content Skip to footer

Creational Design Patterns: Builder

Taming Complex Object Construction

Have you ever seen a constructor with 15 parameters where you can’t remember which boolean is “enabled” and which is “visible”? Or created an object that requires 10 method calls in a specific order before it’s valid? The Builder pattern transforms complex construction into an elegant, readable process.

The Problem: Constructor Hell and Complex Initialization

You’re building an email service. Your email needs many optional components:

// Nightmare constructor
public class Email
{
    public Email(
        string to,
        string from,
        string subject,
        string body,
        bool isHtml,
        Priority priority,
        List<string> cc,
        List<string> bcc,
        List<Attachment> attachments,
        string replyTo,
        bool requestReadReceipt,
        DateTime? scheduledTime,
        Dictionary<string, string> headers)
    {
        // Which parameter is which? Easy to mess up!
    }
}

// Usage - error-prone and unreadable
var email = new Email(
    "user@example.com",
    "noreply@company.com",
    "Welcome!",
    "<h1>Hello</h1>",
    true,
    Priority.High,
    null,  // What's this?
    null,  // And this?
    null,  // Help!
    null,
    false,
    null,
    null
);

Problems:

  • Difficult to remember parameter order
  • Optional parameters create multiple constructor overloads
  • No validation during construction
  • Unreadable at call site
  • Hard to extend without breaking existing code

The Solution: Builder Pattern

The Builder pattern constructs complex objects step by step, allowing you to create different representations using the same construction process.

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

Pseudocode Structure

Builder (interface)
├── BuildPartA()
├── BuildPartB()
└── GetResult() : Product

ConcreteBuilder : Builder
└── Implements all build methods

Director (optional)
└── Construct(builder) : orchestrates build steps

Product
└── The complex object being built

Real-World Example: HTTP Request Builder

Step 1: Define the Complex Product

public class HttpRequest
{
    public string Url { get; init; }
    public HttpMethod Method { get; init; }
    public Dictionary<string, string> Headers { get; init; } = new();
    public Dictionary<string, string> QueryParameters { get; init; } = new();
    public object? Body { get; init; }
    public TimeSpan Timeout { get; init; }
    public int MaxRetries { get; init; }
    public bool FollowRedirects { get; init; }
    public string? BearerToken { get; init; }

    public override string ToString()
    {
        return $"{Method} {Url}\n" +
               $"Headers: {Headers.Count}\n" +
               $"Query Params: {QueryParameters.Count}\n" +
               $"Timeout: {Timeout.TotalSeconds}s\n" +
               $"Retries: {MaxRetries}";
    }
}

Step 2: Implement the Builder

public class HttpRequestBuilder
{
    private string _url = string.Empty;
    private HttpMethod _method = HttpMethod.Get;
    private readonly Dictionary<string, string> _headers = new();
    private readonly Dictionary<string, string> _queryParams = new();
    private object? _body;
    private TimeSpan _timeout = TimeSpan.FromSeconds(30);
    private int _maxRetries = 3;
    private bool _followRedirects = true;
    private string? _bearerToken;

    public HttpRequestBuilder WithUrl(string url)
    {
        _url = url;
        return this;
    }

    public HttpRequestBuilder WithMethod(HttpMethod method)
    {
        _method = method;
        return this;
    }

    public HttpRequestBuilder WithHeader(string key, string value)
    {
        _headers[key] = value;
        return this;
    }

    public HttpRequestBuilder WithQueryParameter(string key, string value)
    {
        _queryParams[key] = value;
        return this;
    }

    public HttpRequestBuilder WithBody(object body)
    {
        _body = body;
        return this;
    }

    public HttpRequestBuilder WithTimeout(TimeSpan timeout)
    {
        _timeout = timeout;
        return this;
    }

    public HttpRequestBuilder WithRetries(int maxRetries)
    {
        _maxRetries = maxRetries;
        return this;
    }

    public HttpRequestBuilder WithFollowRedirects(bool follow)
    {
        _followRedirects = follow;
        return this;
    }

    public HttpRequestBuilder WithBearerToken(string token)
    {
        _bearerToken = token;
        _headers["Authorization"] = $"Bearer {token}";
        return this;
    }

    public HttpRequestBuilder WithBasicAuth(string username, string password)
    {
        var credentials = Convert.ToBase64String(
            System.Text.Encoding.UTF8.GetBytes($"{username}:{password}"));
        _headers["Authorization"] = $"Basic {credentials}";
        return this;
    }

    public HttpRequest Build()
    {
        if (string.IsNullOrWhiteSpace(_url))
            throw new InvalidOperationException("URL is required");

        return new HttpRequest
        {
            Url = _url,
            Method = _method,
            Headers = new Dictionary<string, string>(_headers),
            QueryParameters = new Dictionary<string, string>(_queryParams),
            Body = _body,
            Timeout = _timeout,
            MaxRetries = _maxRetries,
            FollowRedirects = _followRedirects,
            BearerToken = _bearerToken
        };
    }
}

Step 3: Use the Builder

public class Program
{
    public static async Task Main(string[] args)
    {
        // Fluent, readable construction
        var request = new HttpRequestBuilder()
            .WithUrl("https://api.example.com/users")
            .WithMethod(HttpMethod.Post)
            .WithBearerToken("eyJhbGciOiJIUzI1NiIs...")
            .WithHeader("Content-Type", "application/json")
            .WithQueryParameter("page", "1")
            .WithQueryParameter("limit", "50")
            .WithBody(new { name = "John Doe", email = "john@example.com" })
            .WithTimeout(TimeSpan.FromSeconds(60))
            .WithRetries(5)
            .Build();

        Console.WriteLine(request);

        // Different configuration, same builder
        var simpleRequest = new HttpRequestBuilder()
            .WithUrl("https://api.example.com/health")
            .WithTimeout(TimeSpan.FromSeconds(5))
            .Build();

        Console.WriteLine("\n" + simpleRequest);
    }
}

Advanced Example: SQL Query Builder

public class SqlQuery
{
    public string Query { get; init; } = string.Empty;
    public Dictionary<string, object> Parameters { get; init; } = new();

    public override string ToString() => Query;
}

public class SqlQueryBuilder
{
    private readonly List<string> _selectColumns = new();
    private string? _fromTable;
    private readonly List<string> _whereClauses = new();
    private readonly List<string> _orderBy = new();
    private readonly Dictionary<string, object> _parameters = new();
    private int _limit;
    private int _offset;
    private int _parameterCounter = 0;

    public SqlQueryBuilder Select(params string[] columns)
    {
        _selectColumns.AddRange(columns);
        return this;
    }

    public SqlQueryBuilder From(string table)
    {
        _fromTable = table;
        return this;
    }

    public SqlQueryBuilder Where(string column, string op, object value)
    {
        var paramName = $"@p{_parameterCounter++}";
        _whereClauses.Add($"{column} {op} {paramName}");
        _parameters[paramName] = value;
        return this;
    }

    public SqlQueryBuilder OrderBy(string column, bool descending = false)
    {
        _orderBy.Add($"{column} {(descending ? "DESC" : "ASC")}");
        return this;
    }

    public SqlQueryBuilder Limit(int limit)
    {
        _limit = limit;
        return this;
    }

    public SqlQueryBuilder Offset(int offset)
    {
        _offset = offset;
        return this;
    }

    public SqlQuery Build()
    {
        if (_fromTable == null)
            throw new InvalidOperationException("FROM clause is required");

        var query = new System.Text.StringBuilder();
        
        query.Append("SELECT ");
        query.Append(_selectColumns.Count > 0 
            ? string.Join(", ", _selectColumns) 
            : "*");
        
        query.Append($" FROM {_fromTable}");

        if (_whereClauses.Count > 0)
        {
            query.Append(" WHERE ");
            query.Append(string.Join(" AND ", _whereClauses));
        }

        if (_orderBy.Count > 0)
        {
            query.Append(" ORDER BY ");
            query.Append(string.Join(", ", _orderBy));
        }

        if (_limit > 0)
            query.Append($" LIMIT {_limit}");

        if (_offset > 0)
            query.Append($" OFFSET {_offset}");

        return new SqlQuery
        {
            Query = query.ToString(),
            Parameters = new Dictionary<string, object>(_parameters)
        };
    }
}

// Usage
var query = new SqlQueryBuilder()
    .Select("Id", "Name", "Email")
    .From("Users")
    .Where("Age", ">=", 18)
    .Where("IsActive", "=", true)
    .OrderBy("CreatedAt", descending: true)
    .Limit(20)
    .Offset(40)
    .Build();

Console.WriteLine(query);
// SELECT Id, Name, Email FROM Users WHERE Age >= @p0 AND IsActive = @p1 ORDER BY CreatedAt DESC LIMIT 20 OFFSET 40

Pattern with Director

public class EmailDirector
{
    public HttpRequest BuildWelcomeEmail(string userEmail)
    {
        return new HttpRequestBuilder()
            .WithUrl("https://api.sendgrid.com/v3/mail/send")
            .WithMethod(HttpMethod.Post)
            .WithBearerToken("SG.xxx")
            .WithBody(new
            {
                personalizations = new[]
                {
                    new { to = new[] { new { email = userEmail } } }
                },
                from = new { email = "welcome@company.com" },
                subject = "Welcome!",
                content = new[] { new { type = "text/html", value = "<h1>Welcome</h1>" } }
            })
            .Build();
    }

    public HttpRequest BuildPasswordResetEmail(string userEmail, string resetToken)
    {
        return new HttpRequestBuilder()
            .WithUrl("https://api.sendgrid.com/v3/mail/send")
            .WithMethod(HttpMethod.Post)
            .WithBearerToken("SG.xxx")
            .WithBody(new
            {
                personalizations = new[]
                {
                    new { to = new[] { new { email = userEmail } } }
                },
                from = new { email = "security@company.com" },
                subject = "Password Reset",
                content = new[] { new { type = "text/html", value = $"<a href='reset?token={resetToken}'>Reset</a>" } }
            })
            .Build();
    }
}

Enterprise Applications

1. Configuration Management

Build complex application configurations with validation and defaults.

2. Report Generators

Construct detailed reports with optional sections, filters, and formatting.

3. Test Data Builders

Create test objects with sensible defaults and customizable properties.

4. ORM Query Builders

Build database queries programmatically with type safety.

5. API Client Builders

Configure HTTP clients with authentication, headers, and retry policies.

Benefits

  • Readability: Construction code is self-documenting
  • Flexibility: Build variations of the same object easily
  • Immutability: Final product can be immutable
  • Validation: Centralize validation logic in the Build method
  • Testability: Easy to create test objects with specific configurations

When to Use

Use Builder when:

  • Object construction requires many optional parameters
  • The construction process must allow different representations
  • You want to construct an object step-by-step
  • Creating the object requires complex validation or setup

When NOT to Use

Avoid this pattern when:

  • The object is simple with few properties
  • Construction is straightforward with no variations
  • The overhead of the builder outweighs its benefits
  • You can use object initializers effectively

Key Takeaway: The Builder pattern transforms complex object construction from an error-prone, unreadable mess into a clear, fluent, and maintainable process, making your code self-documenting and your objects easier to create correctly.

Leave a Comment