BrandGhost
Builder Pattern Real-World Example in C#: Complete Implementation

Builder Pattern Real-World Example in C#: Complete Implementation

Builder Pattern Real-World Example in C#: Complete Implementation

The best way to understand the Builder pattern is through a complete, real-world example. In this article, we'll build a configuration system that demonstrates how Builder manages complex object construction—exactly the scenario this pattern was designed to solve.

We'll create a system where applications can configure database connections with multiple optional settings (connection string, timeout, retry policy, logging, etc.), and the system ensures all configurations are valid before creating the connection object. This is a classic Builder use case: multiple optional parameters that must be validated together.

By the end of this article, you'll have a complete, working C# implementation that you can run and modify. We'll walk through each component, explain the design decisions, and show how Builder solves the problem elegantly.

This article focuses on: Narrative application and evolution of Builder in a real-world scenario. We emphasize why Builder matters here and how it scales. For step-by-step implementation mechanics, see How to Implement Builder Pattern. For conceptual foundation, see Builder Design Pattern: Complete Guide. For decision-making guidance, see When to Use Builder Pattern.

Editorial Note: This example is a teaching model designed to illustrate Builder pattern concepts in a real-world context. It is not a prescribed production architecture. In production, you would adapt this approach based on your specific requirements, validation needs, and architectural constraints. For production-ready validation strategies and code organization, see Builder Pattern Best Practices.

For foundational knowledge, explore The Big List of Design Patterns for context on all design patterns.

Problem: Database Connection Configuration

Imagine you're building a database connection management system. Applications need to configure database connections with various settings:

  • Connection String: Required - the database server and database name
  • Command Timeout: Optional - how long to wait for commands (default: 30 seconds)
  • Retry Policy: Optional - how to handle transient failures
  • Logging: Optional - whether to log connection events
  • Connection Pooling: Optional - pool size and settings
  • Encryption: Optional - SSL/TLS settings

The challenge: When an application configures a database connection, all settings must be valid and compatible. You can't create a connection with invalid timeout values, incompatible retry settings, or missing required fields. The Builder pattern ensures all configurations are validated before the connection object is created.

Solution: Builder Pattern

Builder is perfect here because we need to ensure all connection settings are valid and compatible before creating the connection. This example demonstrates why Builder matters in real-world scenarios and how it scales as requirements evolve. For detailed step-by-step implementation mechanics, see How to Implement Builder Pattern. Let's implement it step by step.

Step 1: Define the Product

First, we define the connection configuration object that will be built:

// Product: Database Connection Configuration
public class DatabaseConnectionConfig
{
    public string ConnectionString { get; set; }
    public int CommandTimeoutSeconds { get; set; }
    public RetryPolicy RetryPolicy { get; set; }
    public bool EnableLogging { get; set; }
    public ConnectionPoolSettings PoolSettings { get; set; }
    public EncryptionSettings EncryptionSettings { get; set; }
    
    public void Validate()
    {
        if (string.IsNullOrWhiteSpace(ConnectionString))
            throw new InvalidOperationException("Connection string is required");
            
        if (CommandTimeoutSeconds < 1 || CommandTimeoutSeconds > 300)
            throw new InvalidOperationException("Command timeout must be between 1 and 300 seconds");
    }
}

// Supporting classes
public class RetryPolicy
{
    public int MaxRetries { get; set; }
    public TimeSpan DelayBetweenRetries { get; set; }
    public bool ExponentialBackoff { get; set; }
}

public class ConnectionPoolSettings
{
    public int MinPoolSize { get; set; }
    public int MaxPoolSize { get; set; }
    public TimeSpan ConnectionLifetime { get; set; }
}

public class EncryptionSettings
{
    public bool RequireEncryption { get; set; }
    public string CertificateThumbprint { get; set; }
    public bool TrustServerCertificate { get; set; }
}

Step 2: Create the Builder Interface

Next, we define the builder interface that specifies how to construct the configuration:

// Builder Interface
public interface IDatabaseConnectionConfigBuilder
{
    IDatabaseConnectionConfigBuilder SetConnectionString(string server, string database);
    IDatabaseConnectionConfigBuilder SetConnectionString(string connectionString);
    IDatabaseConnectionConfigBuilder SetCommandTimeout(int seconds);
    IDatabaseConnectionConfigBuilder WithRetryPolicy(int maxRetries, TimeSpan delay);
    IDatabaseConnectionConfigBuilder WithRetryPolicy(int maxRetries, TimeSpan delay, bool exponentialBackoff);
    IDatabaseConnectionConfigBuilder EnableLogging();
    IDatabaseConnectionConfigBuilder WithConnectionPooling(int minSize, int maxSize);
    IDatabaseConnectionConfigBuilder WithConnectionPooling(int minSize, int maxSize, TimeSpan lifetime);
    IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption);
    IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption, string certificateThumbprint);
    IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption, bool trustServerCertificate);
    DatabaseConnectionConfig Build();
}

Step 3: Implement the Concrete Builder

Now we implement the builder with sensible defaults. Note that this is a simplified teaching example—production implementations would include more comprehensive validation and error handling (see Builder Pattern Best Practices for guidance):

// Concrete Builder
public class DatabaseConnectionConfigBuilder : IDatabaseConnectionConfigBuilder
{
    private DatabaseConnectionConfig _config = new();
    
    public IDatabaseConnectionConfigBuilder SetConnectionString(string server, string database)
    {
        if (string.IsNullOrWhiteSpace(server))
            throw new ArgumentException("Server cannot be empty", nameof(server));
        if (string.IsNullOrWhiteSpace(database))
            throw new ArgumentException("Database cannot be empty", nameof(database));
            
        _config.ConnectionString = $"Server={server};Database={database};Integrated Security=true;";
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder SetConnectionString(string connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
            throw new ArgumentException("Connection string cannot be empty", nameof(connectionString));
            
        _config.ConnectionString = connectionString;
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder SetCommandTimeout(int seconds)
    {
        if (seconds < 1 || seconds > 300)
            throw new ArgumentException("Command timeout must be between 1 and 300 seconds", nameof(seconds));
            
        _config.CommandTimeoutSeconds = seconds;
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder WithRetryPolicy(int maxRetries, TimeSpan delay)
    {
        return WithRetryPolicy(maxRetries, delay, false);
    }
    
    public IDatabaseConnectionConfigBuilder WithRetryPolicy(int maxRetries, TimeSpan delay, bool exponentialBackoff)
    {
        if (maxRetries < 0 || maxRetries > 10)
            throw new ArgumentException("Max retries must be between 0 and 10", nameof(maxRetries));
        if (delay.TotalMilliseconds < 0)
            throw new ArgumentException("Delay cannot be negative", nameof(delay));
            
        _config.RetryPolicy = new RetryPolicy
        {
            MaxRetries = maxRetries,
            DelayBetweenRetries = delay,
            ExponentialBackoff = exponentialBackoff
        };
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder EnableLogging()
    {
        _config.EnableLogging = true;
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder WithConnectionPooling(int minSize, int maxSize)
    {
        return WithConnectionPooling(minSize, maxSize, TimeSpan.FromMinutes(30));
    }
    
    public IDatabaseConnectionConfigBuilder WithConnectionPooling(int minSize, int maxSize, TimeSpan lifetime)
    {
        if (minSize < 0)
            throw new ArgumentException("Min pool size cannot be negative", nameof(minSize));
        if (maxSize < minSize)
            throw new ArgumentException("Max pool size must be greater than or equal to min pool size", nameof(maxSize));
        if (lifetime.TotalSeconds < 0)
            throw new ArgumentException("Connection lifetime cannot be negative", nameof(lifetime));
            
        _config.PoolSettings = new ConnectionPoolSettings
        {
            MinPoolSize = minSize,
            MaxPoolSize = maxSize,
            ConnectionLifetime = lifetime
        };
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption)
    {
        return WithEncryption(requireEncryption, false);
    }
    
    public IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption, string certificateThumbprint)
    {
        if (requireEncryption && string.IsNullOrWhiteSpace(certificateThumbprint))
            throw new ArgumentException("Certificate thumbprint is required when encryption is enabled", nameof(certificateThumbprint));
            
        _config.EncryptionSettings = new EncryptionSettings
        {
            RequireEncryption = requireEncryption,
            CertificateThumbprint = certificateThumbprint,
            TrustServerCertificate = false
        };
        return this;
    }
    
    public IDatabaseConnectionConfigBuilder WithEncryption(bool requireEncryption, bool trustServerCertificate)
    {
        _config.EncryptionSettings = new EncryptionSettings
        {
            RequireEncryption = requireEncryption,
            TrustServerCertificate = trustServerCertificate
        };
        return this;
    }
    
    public DatabaseConnectionConfig Build()
    {
        // Set defaults for optional properties
        if (_config.CommandTimeoutSeconds == 0)
            _config.CommandTimeoutSeconds = 30; // Default timeout
            
        // Validate the complete configuration
        _config.Validate();
        
        // Return the configuration
        return _config;
    }
}

Step 4: Using the Builder

Now we can use the builder to create configurations:

// Example 1: Simple configuration
var simpleConfig = new DatabaseConnectionConfigBuilder()
    .SetConnectionString("localhost", "MyDatabase")
    .Build();

// Example 2: Configuration with retry policy
var configWithRetry = new DatabaseConnectionConfigBuilder()
    .SetConnectionString("localhost", "MyDatabase")
    .SetCommandTimeout(60)
    .WithRetryPolicy(maxRetries: 3, delay: TimeSpan.FromSeconds(1), exponentialBackoff: true)
    .EnableLogging()
    .Build();

// Example 3: Full configuration
var fullConfig = new DatabaseConnectionConfigBuilder()
    .SetConnectionString("Server=prod-server;Database=ProductionDB;")
    .SetCommandTimeout(120)
    .WithRetryPolicy(maxRetries: 5, delay: TimeSpan.FromSeconds(2), exponentialBackoff: true)
    .EnableLogging()
    .WithConnectionPooling(minSize: 5, maxSize: 50, lifetime: TimeSpan.FromHours(1))
    .WithEncryption(requireEncryption: true, trustServerCertificate: false)
    .Build();

Step 5: Using the Builder in Application Code

The builder integrates seamlessly into application code:

// Configure using Builder
var config = new DatabaseConnectionConfigBuilder()
    .SetConnectionString("localhost", "MyApp")
    .SetCommandTimeout(60)
    .WithRetryPolicy(maxRetries: 3, delay: TimeSpan.FromSeconds(1))
    .EnableLogging()
    .Build();

// Use the configuration to create database connections
var connection = new SqlConnection(config.ConnectionString);
connection.ConnectionTimeout = config.CommandTimeoutSeconds;

Advanced: Builder with Director

For common configurations, we can add a Director:

// Director for common configurations
public class DatabaseConfigDirector
{
    public DatabaseConnectionConfig BuildDevelopmentConfig(IDatabaseConnectionConfigBuilder builder)
    {
        return builder
            .SetConnectionString("localhost", "DevDatabase")
            .SetCommandTimeout(30)
            .EnableLogging()
            .Build();
    }
    
    public DatabaseConnectionConfig BuildProductionConfig(IDatabaseConnectionConfigBuilder builder)
    {
        return builder
            .SetConnectionString("prod-server", "ProductionDB")
            .SetCommandTimeout(120)
            .WithRetryPolicy(maxRetries: 5, delay: TimeSpan.FromSeconds(2), exponentialBackoff: true)
            .EnableLogging()
            .WithConnectionPooling(minSize: 10, maxSize: 100, lifetime: TimeSpan.FromHours(2))
            .WithEncryption(requireEncryption: true, trustServerCertificate: false)
            .Build();
    }
    
    public DatabaseConnectionConfig BuildTestConfig(IDatabaseConnectionConfigBuilder builder)
    {
        return builder
            .SetConnectionString("test-server", "TestDatabase")
            .SetCommandTimeout(10)
            .Build();
    }
}

// Usage
var director = new DatabaseConfigDirector();
var builder = new DatabaseConnectionConfigBuilder();

var prodConfig = director.BuildProductionConfig(builder);
var devConfig = director.BuildDevelopmentConfig(builder);

Testing the Builder

The builder can be tested to ensure it creates valid configurations:

[TestMethod]
public void Build_WithRequiredFields_Succeeds()
{
    var config = new DatabaseConnectionConfigBuilder()
        .SetConnectionString("localhost", "TestDB")
        .Build();
        
    Assert.IsNotNull(config);
    Assert.IsTrue(config.ConnectionString.Contains("localhost"));
    Assert.AreEqual(30, config.CommandTimeoutSeconds); // Default
}

[TestMethod]
public void Build_WithoutConnectionString_ThrowsException()
{
    var builder = new DatabaseConnectionConfigBuilder();
    Assert.ThrowsException<InvalidOperationException>(() => builder.Build());
}

Why Builder Matters Here

This example demonstrates why Builder is valuable:

  1. Many Optional Parameters: Database configuration has many optional settings
  2. Complex Validation: Settings must be validated individually and together
  3. Readability: Builder makes configuration code readable and maintainable
  4. Safety: Validation ensures invalid configurations can't be created
  5. Flexibility: Easy to add new configuration options without breaking existing code

Conclusion

This real-world example shows Builder pattern in action, constructing complex database connection configurations with validation, defaults, and cross-property rules. The pattern ensures configurations are always valid and makes the code readable and maintainable.

By using Builder, we've created a system that's easy to use, safe, and extensible. As requirements evolve and new configuration options are added, the Builder pattern makes it easy to extend without breaking existing code.

Frequently Asked Questions

Why not use a constructor with optional parameters?

With 10+ optional parameters, constructors become unreadable. Builder provides a clear, fluent interface that's easier to use and maintain.

Can I use this pattern with dependency injection?

Yes! Register the builder in your DI container and inject it into services that need to create configurations.

What if I need to modify a configuration after building?

If you need mutable configurations, Builder still works. However, consider whether immutability would be better for your use case.

How do I add new configuration options?

Add a new method to the builder interface and implementation. Existing code continues to work, and new code can use the new option.

Is this pattern overkill for simple configurations?

Yes. If you only have 2-3 optional parameters, use a constructor or object initializer. Builder adds value when you have 5+ optional parameters or complex validation.

Can I have multiple builders for the same product?

Yes! You can have different builders that create the same product type but with different construction strategies or validation rules.

How do I handle configuration from configuration files?

Create a builder method that accepts a configuration object or reads from a file, then uses the builder's fluent interface to set properties.

Builder Design Pattern in C#: Complete Guide with Examples

Master the Builder design pattern in C# with code examples, real-world scenarios, and implementation guidance for constructing complex objects step by step.

How to Implement Builder Pattern in C#: Step-by-Step Guide

Learn how to implement Builder pattern in C# with a complete step-by-step guide. Includes code examples, best practices, and common pitfalls to avoid.

Abstract Factory Pattern Real-World Example in C#: Complete Implementation

See Abstract Factory pattern in action with a complete real-world C# example. Step-by-step implementation of a furniture shop system demonstrating families of related objects.

An error has occurred. This application may no longer respond until reloaded. Reload