πŸ“„

Creational Design Patterns

Intermediate 7 min read 1400 words

Creational Design Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.


Factory Method

Intent: Define an interface for creating an object, but let subclasses decide which class to instantiate.

πŸ“– Theory & When to Use

When to Use

  • You don’t know ahead of time what class you need
  • You want subclasses to specify the objects they create
  • You want to localize the knowledge of which class gets created

Real-World Analogy

A logistics company can deliver by truck or ship. Instead of hardcoding the transport type, a factory method lets each logistics branch decide which transport to create.

Key Participants

  • Product: Interface for objects the factory creates
  • ConcreteProduct: Implements the Product interface
  • Creator: Declares the factory method
  • ConcreteCreator: Overrides factory method to return ConcreteProduct
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>> β”‚         β”‚    <<abstract>> β”‚
β”‚     IProduct    β”‚         β”‚     Creator     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€         β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()   β”‚         β”‚ + FactoryMethod()β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚ + SomeOperation()β”‚
         β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚ implements                β”‚ extends
         β”‚                           β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ConcreteProduct │◄────────│ ConcreteCreator β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ creates β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Operation()   β”‚         β”‚ + FactoryMethod()β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface IProduct { void DoWork(); }

public class ConcreteProductA : IProduct
{
    public void DoWork() => Console.WriteLine("Product A");
}

public abstract class Creator
{
    public abstract IProduct CreateProduct();
}

public class ConcreteCreatorA : Creator
{
    public override IProduct CreateProduct() => new ConcreteProductA();
}

// Usage
var creator = new ConcreteCreatorA();
var product = creator.CreateProduct();
product.DoWork();
πŸ’» Medium Example (~50 lines)
// Product interface
public interface IDocument
{
    void Open();
    void Save();
    string GetContent();
}

// Concrete products
public class PdfDocument : IDocument
{
    public void Open() => Console.WriteLine("Opening PDF document");
    public void Save() => Console.WriteLine("Saving PDF document");
    public string GetContent() => "PDF Content";
}

public class WordDocument : IDocument
{
    public void Open() => Console.WriteLine("Opening Word document");
    public void Save() => Console.WriteLine("Saving Word document");
    public string GetContent() => "Word Content";
}

// Creator
public abstract class DocumentCreator
{
    public abstract IDocument CreateDocument();

    public void OpenAndEdit()
    {
        var doc = CreateDocument();
        doc.Open();
        Console.WriteLine($"Editing: {doc.GetContent()}");
        doc.Save();
    }
}

// Concrete creators
public class PdfCreator : DocumentCreator
{
    public override IDocument CreateDocument() => new PdfDocument();
}

public class WordCreator : DocumentCreator
{
    public override IDocument CreateDocument() => new WordDocument();
}

// Usage
DocumentCreator creator = new PdfCreator();
creator.OpenAndEdit();
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Product interface with async support
public interface INotificationSender
{
    Task<bool> SendAsync(string recipient, string message, CancellationToken ct = default);
    string ProviderName { get; }
}

// Concrete products
public class EmailNotificationSender : INotificationSender
{
    private readonly ILogger<EmailNotificationSender> _logger;
    private readonly EmailSettings _settings;

    public EmailNotificationSender(ILogger<EmailNotificationSender> logger, EmailSettings settings)
    {
        _logger = logger;
        _settings = settings;
    }

    public string ProviderName => "Email";

    public async Task<bool> SendAsync(string recipient, string message, CancellationToken ct = default)
    {
        try
        {
            _logger.LogInformation("Sending email to {Recipient}", recipient);
            // Simulate async email sending
            await Task.Delay(100, ct);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send email to {Recipient}", recipient);
            return false;
        }
    }
}

public class SmsNotificationSender : INotificationSender
{
    private readonly ILogger<SmsNotificationSender> _logger;
    private readonly SmsSettings _settings;

    public SmsNotificationSender(ILogger<SmsNotificationSender> logger, SmsSettings settings)
    {
        _logger = logger;
        _settings = settings;
    }

    public string ProviderName => "SMS";

    public async Task<bool> SendAsync(string recipient, string message, CancellationToken ct = default)
    {
        try
        {
            _logger.LogInformation("Sending SMS to {Recipient}", recipient);
            await Task.Delay(50, ct);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send SMS to {Recipient}", recipient);
            return false;
        }
    }
}

// Factory interface
public interface INotificationSenderFactory
{
    INotificationSender Create(NotificationType type);
}

// Factory implementation with DI
public class NotificationSenderFactory : INotificationSenderFactory
{
    private readonly IServiceProvider _serviceProvider;

    public NotificationSenderFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public INotificationSender Create(NotificationType type) => type switch
    {
        NotificationType.Email => _serviceProvider.GetRequiredService<EmailNotificationSender>(),
        NotificationType.Sms => _serviceProvider.GetRequiredService<SmsNotificationSender>(),
        _ => throw new ArgumentOutOfRangeException(nameof(type), $"Unknown notification type: {type}")
    };
}

public enum NotificationType { Email, Sms }

// Settings classes
public record EmailSettings(string SmtpServer, int Port);
public record SmsSettings(string ApiKey, string FromNumber);

// DI Registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddNotificationServices(this IServiceCollection services)
    {
        services.AddSingleton<EmailSettings>(new EmailSettings("smtp.example.com", 587));
        services.AddSingleton<SmsSettings>(new SmsSettings("api-key", "+1234567890"));
        services.AddTransient<EmailNotificationSender>();
        services.AddTransient<SmsNotificationSender>();
        services.AddSingleton<INotificationSenderFactory, NotificationSenderFactory>();
        return services;
    }
}

// Usage in a service
public class NotificationService
{
    private readonly INotificationSenderFactory _factory;
    private readonly ILogger<NotificationService> _logger;

    public NotificationService(INotificationSenderFactory factory, ILogger<NotificationService> logger)
    {
        _factory = factory;
        _logger = logger;
    }

    public async Task NotifyUserAsync(string userId, string message, NotificationType preferredChannel)
    {
        var sender = _factory.Create(preferredChannel);
        _logger.LogInformation("Using {Provider} to notify user {UserId}", sender.ProviderName, userId);
        await sender.SendAsync(userId, message);
    }
}
❓ Interview Q&A

Q1: What’s the difference between Factory Method and Simple Factory? A1: Simple Factory is a single class with a method that creates objects based on parameters. Factory Method uses inheritance - subclasses override a method to create specific products. Factory Method follows OCP better.

Q2: When would you use Factory Method over direct instantiation? A2: When the exact type isn’t known until runtime, when you want to decouple creation from usage, or when subclasses need to determine which objects to create.

Q3: How does Factory Method support the Open/Closed Principle? A3: New product types can be added by creating new ConcreteCreator subclasses without modifying existing code.


Abstract Factory

Intent: Provide an interface for creating families of related objects without specifying their concrete classes.

πŸ“– Theory & When to Use

When to Use

  • System should be independent of how products are created
  • System needs to work with multiple families of products
  • Related products must be used together
  • You want to provide a library of products without exposing implementations

Real-World Analogy

A furniture store sells matching sets (Victorian, Modern, Art Deco). Each set includes a chair, sofa, and table that match each other. The Abstract Factory ensures you get a complete matching set.

Key Participants

  • AbstractFactory: Declares creation methods for each product type
  • ConcreteFactory: Implements creation methods for a product family
  • AbstractProduct: Interface for a type of product
  • ConcreteProduct: Implements a product for a specific family
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  <<interface>>      β”‚
β”‚  IAbstractFactory   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + CreateProductA()  β”‚
β”‚ + CreateProductB()  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
     β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
     β”‚           β”‚
β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”
β”‚Factory1 β”‚ β”‚Factory2 β”‚
β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
     β”‚           β”‚
     β–Ό           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ProductA1β”‚ β”‚ProductA2β”‚
β”‚ProductB1β”‚ β”‚ProductB2β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface IButton { void Render(); }
public interface ICheckbox { void Check(); }

public interface IGUIFactory
{
    IButton CreateButton();
    ICheckbox CreateCheckbox();
}

public class WinButton : IButton { public void Render() => Console.WriteLine("Windows Button"); }
public class WinCheckbox : ICheckbox { public void Check() => Console.WriteLine("Windows Checkbox"); }

public class WindowsFactory : IGUIFactory
{
    public IButton CreateButton() => new WinButton();
    public ICheckbox CreateCheckbox() => new WinCheckbox();
}

// Usage
IGUIFactory factory = new WindowsFactory();
var button = factory.CreateButton();
button.Render();
πŸ’» Medium Example (~50 lines)
// Abstract products
public interface IButton { void Render(); void OnClick(Action action); }
public interface ITextBox { void Render(); string GetText(); }
public interface IPanel { void AddControl(object control); void Render(); }

// Windows family
public class WindowsButton : IButton
{
    public void Render() => Console.WriteLine("[Windows Button]");
    public void OnClick(Action action) => action();
}

public class WindowsTextBox : ITextBox
{
    public void Render() => Console.WriteLine("[Windows TextBox]");
    public string GetText() => "Windows text";
}

public class WindowsPanel : IPanel
{
    private readonly List<object> _controls = new();
    public void AddControl(object control) => _controls.Add(control);
    public void Render() => Console.WriteLine($"Windows Panel with {_controls.Count} controls");
}

// Mac family
public class MacButton : IButton
{
    public void Render() => Console.WriteLine("(Mac Button)");
    public void OnClick(Action action) => action();
}

public class MacTextBox : ITextBox
{
    public void Render() => Console.WriteLine("(Mac TextBox)");
    public string GetText() => "Mac text";
}

public class MacPanel : IPanel
{
    private readonly List<object> _controls = new();
    public void AddControl(object control) => _controls.Add(control);
    public void Render() => Console.WriteLine($"Mac Panel with {_controls.Count} controls");
}

// Abstract factory
public interface IGUIFactory
{
    IButton CreateButton();
    ITextBox CreateTextBox();
    IPanel CreatePanel();
}

public class WindowsFactory : IGUIFactory
{
    public IButton CreateButton() => new WindowsButton();
    public ITextBox CreateTextBox() => new WindowsTextBox();
    public IPanel CreatePanel() => new WindowsPanel();
}

public class MacFactory : IGUIFactory
{
    public IButton CreateButton() => new MacButton();
    public ITextBox CreateTextBox() => new MacTextBox();
    public IPanel CreatePanel() => new MacPanel();
}

// Client code
public class Application
{
    private readonly IGUIFactory _factory;

    public Application(IGUIFactory factory) => _factory = factory;

    public void CreateUI()
    {
        var panel = _factory.CreatePanel();
        panel.AddControl(_factory.CreateButton());
        panel.AddControl(_factory.CreateTextBox());
        panel.Render();
    }
}
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

// Abstract products
public interface IDbConnection : IAsyncDisposable
{
    Task OpenAsync(CancellationToken ct = default);
    Task<IDbTransaction> BeginTransactionAsync(CancellationToken ct = default);
    string ConnectionString { get; }
}

public interface IDbCommand : IAsyncDisposable
{
    Task<int> ExecuteNonQueryAsync(string sql, object? parameters = null, CancellationToken ct = default);
    Task<T?> ExecuteScalarAsync<T>(string sql, object? parameters = null, CancellationToken ct = default);
    Task<IEnumerable<T>> QueryAsync<T>(string sql, object? parameters = null, CancellationToken ct = default);
}

public interface IDbTransaction : IAsyncDisposable
{
    Task CommitAsync(CancellationToken ct = default);
    Task RollbackAsync(CancellationToken ct = default);
}

// Abstract factory
public interface IDatabaseFactory
{
    IDbConnection CreateConnection();
    IDbCommand CreateCommand(IDbConnection connection);
    string ProviderName { get; }
}

// SQL Server implementation
public class SqlServerConnection : IDbConnection
{
    public string ConnectionString { get; }
    public SqlServerConnection(string connectionString) => ConnectionString = connectionString;
    public async Task OpenAsync(CancellationToken ct = default) => await Task.Delay(10, ct);
    public async Task<IDbTransaction> BeginTransactionAsync(CancellationToken ct = default)
    {
        await Task.Delay(5, ct);
        return new SqlServerTransaction();
    }
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

public class SqlServerCommand : IDbCommand
{
    private readonly IDbConnection _connection;
    public SqlServerCommand(IDbConnection connection) => _connection = connection;

    public async Task<int> ExecuteNonQueryAsync(string sql, object? parameters = null, CancellationToken ct = default)
    {
        await Task.Delay(10, ct);
        return 1;
    }

    public async Task<T?> ExecuteScalarAsync<T>(string sql, object? parameters = null, CancellationToken ct = default)
    {
        await Task.Delay(10, ct);
        return default;
    }

    public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object? parameters = null, CancellationToken ct = default)
    {
        await Task.Delay(10, ct);
        return Enumerable.Empty<T>();
    }

    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

public class SqlServerTransaction : IDbTransaction
{
    public async Task CommitAsync(CancellationToken ct = default) => await Task.Delay(5, ct);
    public async Task RollbackAsync(CancellationToken ct = default) => await Task.Delay(5, ct);
    public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}

public class SqlServerFactory : IDatabaseFactory
{
    private readonly string _connectionString;
    public string ProviderName => "SqlServer";

    public SqlServerFactory(IOptions<SqlServerOptions> options)
    {
        _connectionString = options.Value.ConnectionString;
    }

    public IDbConnection CreateConnection() => new SqlServerConnection(_connectionString);
    public IDbCommand CreateCommand(IDbConnection connection) => new SqlServerCommand(connection);
}

// PostgreSQL implementation (similar structure)
public class PostgreSqlFactory : IDatabaseFactory
{
    private readonly string _connectionString;
    public string ProviderName => "PostgreSQL";

    public PostgreSqlFactory(IOptions<PostgreSqlOptions> options)
    {
        _connectionString = options.Value.ConnectionString;
    }

    public IDbConnection CreateConnection() => new PostgreSqlConnection(_connectionString);
    public IDbCommand CreateCommand(IDbConnection connection) => new PostgreSqlCommand(connection);
}

// Configuration
public record SqlServerOptions { public string ConnectionString { get; init; } = ""; }
public record PostgreSqlOptions { public string ConnectionString { get; init; } = ""; }
public record DatabaseOptions { public string Provider { get; init; } = "SqlServer"; }

// Factory provider for runtime selection
public class DatabaseFactoryProvider
{
    private readonly IServiceProvider _serviceProvider;
    private readonly DatabaseOptions _options;

    public DatabaseFactoryProvider(IServiceProvider serviceProvider, IOptions<DatabaseOptions> options)
    {
        _serviceProvider = serviceProvider;
        _options = options.Value;
    }

    public IDatabaseFactory GetFactory() => _options.Provider switch
    {
        "SqlServer" => _serviceProvider.GetRequiredService<SqlServerFactory>(),
        "PostgreSQL" => _serviceProvider.GetRequiredService<PostgreSqlFactory>(),
        _ => throw new InvalidOperationException($"Unknown database provider: {_options.Provider}")
    };
}

// DI Registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddDatabaseFactories(this IServiceCollection services, IConfiguration config)
    {
        services.Configure<SqlServerOptions>(config.GetSection("SqlServer"));
        services.Configure<PostgreSqlOptions>(config.GetSection("PostgreSQL"));
        services.Configure<DatabaseOptions>(config.GetSection("Database"));

        services.AddSingleton<SqlServerFactory>();
        services.AddSingleton<PostgreSqlFactory>();
        services.AddSingleton<DatabaseFactoryProvider>();

        return services;
    }
}

// Usage in repository
public class UserRepository
{
    private readonly IDatabaseFactory _dbFactory;

    public UserRepository(DatabaseFactoryProvider factoryProvider)
    {
        _dbFactory = factoryProvider.GetFactory();
    }

    public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
    {
        await using var connection = _dbFactory.CreateConnection();
        await connection.OpenAsync(ct);

        await using var command = _dbFactory.CreateCommand(connection);
        var users = await command.QueryAsync<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = id }, ct);
        return users.FirstOrDefault();
    }
}
❓ Interview Q&A

Q1: What’s the difference between Abstract Factory and Factory Method? A1: Factory Method creates one product type using inheritance. Abstract Factory creates families of related products using composition. Abstract Factory often uses Factory Methods internally.

Q2: When would you choose Abstract Factory? A2: When you need to create multiple related objects that must work together, like UI components for different platforms or database access objects for different providers.

Q3: What’s a drawback of Abstract Factory? A3: Adding new product types requires changing the factory interface and all concrete factories. It’s designed for stable product hierarchies.


Builder

Intent: Separate the construction of a complex object from its representation, allowing the same construction process to create different representations.

πŸ“– Theory & When to Use

When to Use

  • Object construction requires many steps
  • Object can have different representations
  • You want to avoid β€œtelescoping constructor” anti-pattern
  • Construction must allow different configurations

Real-World Analogy

Building a house: the same construction process (foundation, walls, roof, interior) can create different houses (wooden cottage, stone mansion) based on the builder used.

Key Participants

  • Builder: Interface for creating parts of a Product
  • ConcreteBuilder: Constructs and assembles parts
  • Director: Constructs using the Builder interface
  • Product: The complex object being built
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Director │─────>β”‚  <<interface>>  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€      β”‚     IBuilder    β”‚
β”‚+ Construct()    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚+ BuildPartA()   β”‚
                  β”‚+ BuildPartB()   β”‚
                  β”‚+ GetResult()    β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚ ConcreteBuilder │───────> Product
                  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                  β”‚+ BuildPartA()   β”‚
                  β”‚+ BuildPartB()   β”‚
                  β”‚+ GetResult()    β”‚
                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public class Pizza
{
    public string Dough { get; set; } = "";
    public string Sauce { get; set; } = "";
    public List<string> Toppings { get; } = new();
}

public class PizzaBuilder
{
    private readonly Pizza _pizza = new();

    public PizzaBuilder SetDough(string dough) { _pizza.Dough = dough; return this; }
    public PizzaBuilder SetSauce(string sauce) { _pizza.Sauce = sauce; return this; }
    public PizzaBuilder AddTopping(string topping) { _pizza.Toppings.Add(topping); return this; }
    public Pizza Build() => _pizza;
}

// Usage (Fluent Builder)
var pizza = new PizzaBuilder()
    .SetDough("Thin")
    .SetSauce("Tomato")
    .AddTopping("Cheese")
    .AddTopping("Pepperoni")
    .Build();
πŸ’» Medium Example (~50 lines)
// Product
public class Computer
{
    public string CPU { get; set; } = "";
    public string RAM { get; set; } = "";
    public string Storage { get; set; } = "";
    public string GPU { get; set; } = "";
    public bool HasWifi { get; set; }

    public override string ToString() =>
        $"CPU: {CPU}, RAM: {RAM}, Storage: {Storage}, GPU: {GPU}, WiFi: {HasWifi}";
}

// Builder interface
public interface IComputerBuilder
{
    IComputerBuilder SetCPU(string cpu);
    IComputerBuilder SetRAM(string ram);
    IComputerBuilder SetStorage(string storage);
    IComputerBuilder SetGPU(string gpu);
    IComputerBuilder SetWifi(bool hasWifi);
    Computer Build();
}

// Concrete builder
public class GamingComputerBuilder : IComputerBuilder
{
    private readonly Computer _computer = new();

    public IComputerBuilder SetCPU(string cpu) { _computer.CPU = cpu; return this; }
    public IComputerBuilder SetRAM(string ram) { _computer.RAM = ram; return this; }
    public IComputerBuilder SetStorage(string storage) { _computer.Storage = storage; return this; }
    public IComputerBuilder SetGPU(string gpu) { _computer.GPU = gpu; return this; }
    public IComputerBuilder SetWifi(bool hasWifi) { _computer.HasWifi = hasWifi; return this; }
    public Computer Build() => _computer;
}

// Director
public class ComputerDirector
{
    public Computer BuildGamingPC(IComputerBuilder builder)
    {
        return builder
            .SetCPU("Intel i9")
            .SetRAM("32GB DDR5")
            .SetStorage("2TB NVMe SSD")
            .SetGPU("RTX 4090")
            .SetWifi(true)
            .Build();
    }

    public Computer BuildOfficePC(IComputerBuilder builder)
    {
        return builder
            .SetCPU("Intel i5")
            .SetRAM("16GB DDR4")
            .SetStorage("512GB SSD")
            .SetGPU("Integrated")
            .SetWifi(true)
            .Build();
    }
}

// Usage
var director = new ComputerDirector();
var gamingPC = director.BuildGamingPC(new GamingComputerBuilder());
Console.WriteLine(gamingPC);
πŸ’» Production-Grade Example
using System.Text.Json;

// Immutable product with validation
public sealed record HttpRequest
{
    public required string Url { get; init; }
    public HttpMethod Method { get; init; } = HttpMethod.Get;
    public IReadOnlyDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>();
    public string? Body { get; init; }
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
    public int MaxRetries { get; init; } = 3;
    public bool ThrowOnError { get; init; } = true;
}

// Fluent builder with validation
public class HttpRequestBuilder
{
    private string? _url;
    private HttpMethod _method = HttpMethod.Get;
    private readonly Dictionary<string, string> _headers = new();
    private string? _body;
    private TimeSpan _timeout = TimeSpan.FromSeconds(30);
    private int _maxRetries = 3;
    private bool _throwOnError = true;

    public HttpRequestBuilder WithUrl(string url)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(url);
        if (!Uri.TryCreate(url, UriKind.Absolute, out _))
            throw new ArgumentException("Invalid URL format", nameof(url));
        _url = url;
        return this;
    }

    public HttpRequestBuilder WithMethod(HttpMethod method)
    {
        _method = method ?? throw new ArgumentNullException(nameof(method));
        return this;
    }

    public HttpRequestBuilder WithHeader(string key, string value)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(key);
        _headers[key] = value ?? throw new ArgumentNullException(nameof(value));
        return this;
    }

    public HttpRequestBuilder WithBearerToken(string token)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(token);
        return WithHeader("Authorization", $"Bearer {token}");
    }

    public HttpRequestBuilder WithJsonBody<T>(T body)
    {
        _body = JsonSerializer.Serialize(body);
        return WithHeader("Content-Type", "application/json");
    }

    public HttpRequestBuilder WithBody(string body, string contentType = "text/plain")
    {
        _body = body;
        return WithHeader("Content-Type", contentType);
    }

    public HttpRequestBuilder WithTimeout(TimeSpan timeout)
    {
        if (timeout <= TimeSpan.Zero)
            throw new ArgumentOutOfRangeException(nameof(timeout), "Timeout must be positive");
        _timeout = timeout;
        return this;
    }

    public HttpRequestBuilder WithRetries(int maxRetries)
    {
        if (maxRetries < 0)
            throw new ArgumentOutOfRangeException(nameof(maxRetries), "Retries cannot be negative");
        _maxRetries = maxRetries;
        return this;
    }

    public HttpRequestBuilder SuppressErrors()
    {
        _throwOnError = false;
        return this;
    }

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

        if (_method == HttpMethod.Get && _body != null)
            throw new InvalidOperationException("GET requests cannot have a body");

        return new HttpRequest
        {
            Url = _url,
            Method = _method,
            Headers = new Dictionary<string, string>(_headers),
            Body = _body,
            Timeout = _timeout,
            MaxRetries = _maxRetries,
            ThrowOnError = _throwOnError
        };
    }

    // Static factory methods for common scenarios
    public static HttpRequestBuilder Get(string url) => new HttpRequestBuilder().WithUrl(url);

    public static HttpRequestBuilder Post(string url) => new HttpRequestBuilder()
        .WithUrl(url)
        .WithMethod(HttpMethod.Post);

    public static HttpRequestBuilder Put(string url) => new HttpRequestBuilder()
        .WithUrl(url)
        .WithMethod(HttpMethod.Put);
}

// Extension for HttpClient integration
public static class HttpClientExtensions
{
    public static async Task<HttpResponseMessage> SendAsync(
        this HttpClient client,
        HttpRequest request,
        CancellationToken ct = default)
    {
        using var httpRequest = new HttpRequestMessage(request.Method, request.Url);

        foreach (var header in request.Headers)
            httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);

        if (request.Body != null)
            httpRequest.Content = new StringContent(request.Body);

        var response = await client.SendAsync(httpRequest, ct);

        if (request.ThrowOnError)
            response.EnsureSuccessStatusCode();

        return response;
    }
}

// Usage
var request = HttpRequestBuilder.Post("https://api.example.com/users")
    .WithBearerToken("my-token")
    .WithJsonBody(new { Name = "John", Email = "john@example.com" })
    .WithTimeout(TimeSpan.FromSeconds(10))
    .WithRetries(3)
    .Build();

using var client = new HttpClient();
var response = await client.SendAsync(request);
❓ Interview Q&A

Q1: What problem does Builder solve that constructors can’t? A1: Builder avoids β€œtelescoping constructors” (many constructor overloads) and makes object creation readable. It also allows step-by-step construction and validation before the object is finalized.

Q2: What’s the difference between Builder and Factory patterns? A2: Factory creates objects in one step. Builder constructs complex objects step by step, allowing different configurations. Builder is for objects with many optional parameters.

Q3: When is the Director class necessary? A3: The Director encapsulates common construction sequences. It’s optionalβ€”clients can use the Builder directly for custom configurations. Director is useful for predefined configurations.


Prototype

Intent: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.

πŸ“– Theory & When to Use

When to Use

  • Object creation is expensive (database calls, complex computations)
  • Objects have many shared properties with few variations
  • You need to avoid subclasses of an object creator
  • Runtime object composition is needed

Real-World Analogy

Copying a document: instead of typing it again, you photocopy the original and make small changes to the copy.

Key Participants

  • Prototype: Interface declaring clone method
  • ConcretePrototype: Implements clone operation
  • Client: Creates new objects by asking prototype to clone itself

Deep vs Shallow Copy

  • Shallow Copy: Copies primitive fields, references point to same objects
  • Deep Copy: Creates independent copies of all referenced objects
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   <<interface>>    β”‚
β”‚     IPrototype     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Clone(): IPrototype
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ConcretePrototype  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - field1           β”‚
β”‚ - field2           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ + Clone(): IPrototype
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public interface IPrototype<T>
{
    T Clone();
}

public class Person : IPrototype<Person>
{
    public string Name { get; set; } = "";
    public int Age { get; set; }

    public Person Clone() => new Person { Name = Name, Age = Age };
}

// Usage
var original = new Person { Name = "John", Age = 30 };
var clone = original.Clone();
clone.Name = "Jane"; // Original unchanged
Console.WriteLine($"{original.Name}, {clone.Name}"); // John, Jane
πŸ’» Medium Example (~50 lines)
public interface IPrototype<T>
{
    T Clone();
    T DeepClone();
}

public class Address
{
    public string Street { get; set; } = "";
    public string City { get; set; } = "";

    public Address Clone() => new Address { Street = Street, City = City };
}

public class Employee : IPrototype<Employee>
{
    public string Name { get; set; } = "";
    public string Department { get; set; } = "";
    public Address Address { get; set; } = new();
    public List<string> Skills { get; set; } = new();

    // Shallow clone - Address and Skills reference same objects
    public Employee Clone()
    {
        return (Employee)MemberwiseClone();
    }

    // Deep clone - completely independent copy
    public Employee DeepClone()
    {
        return new Employee
        {
            Name = Name,
            Department = Department,
            Address = Address.Clone(),
            Skills = new List<string>(Skills)
        };
    }
}

// Usage
var original = new Employee
{
    Name = "John",
    Department = "IT",
    Address = new Address { Street = "123 Main", City = "NYC" },
    Skills = new List<string> { "C#", "SQL" }
};

var shallowClone = original.Clone();
var deepClone = original.DeepClone();

// Modifying shallow clone affects original's Address
shallowClone.Address.City = "LA"; // original.Address.City is now "LA" too!

// Deep clone is independent
deepClone.Address.City = "Chicago"; // original unaffected
πŸ’» Production-Grade Example
using System.Text.Json;

// Generic prototype interface
public interface IDeepCloneable<T> where T : class
{
    T DeepClone();
}

// Base class with JSON-based deep cloning
public abstract class CloneableBase<T> : IDeepCloneable<T> where T : class
{
    private static readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        WriteIndented = false
    };

    public virtual T DeepClone()
    {
        var json = JsonSerializer.Serialize(this, GetType(), _jsonOptions);
        return JsonSerializer.Deserialize<T>(json, _jsonOptions)
            ?? throw new InvalidOperationException("Clone failed");
    }
}

// Domain objects
public class OrderTemplate : CloneableBase<OrderTemplate>
{
    public string TemplateId { get; set; } = Guid.NewGuid().ToString();
    public string CustomerType { get; set; } = "";
    public decimal DiscountPercentage { get; set; }
    public List<OrderLineTemplate> DefaultLines { get; set; } = new();
    public ShippingOptions DefaultShipping { get; set; } = new();
    public Dictionary<string, string> Metadata { get; set; } = new();

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

    // Custom clone with new ID
    public OrderTemplate CloneAsNew()
    {
        var clone = DeepClone();
        clone.TemplateId = Guid.NewGuid().ToString();
        clone.CreatedAt = DateTime.UtcNow;
        return clone;
    }
}

public class OrderLineTemplate
{
    public string ProductSku { get; set; } = "";
    public int DefaultQuantity { get; set; } = 1;
    public decimal UnitPrice { get; set; }
}

public class ShippingOptions
{
    public string Method { get; set; } = "Standard";
    public bool RequireSignature { get; set; }
    public string? SpecialInstructions { get; set; }
}

// Prototype registry for managing templates
public class OrderTemplateRegistry
{
    private readonly Dictionary<string, OrderTemplate> _templates = new();
    private readonly ILogger<OrderTemplateRegistry> _logger;

    public OrderTemplateRegistry(ILogger<OrderTemplateRegistry> logger)
    {
        _logger = logger;
        InitializeDefaultTemplates();
    }

    private void InitializeDefaultTemplates()
    {
        Register("retail", new OrderTemplate
        {
            CustomerType = "Retail",
            DiscountPercentage = 0,
            DefaultShipping = new ShippingOptions { Method = "Standard" }
        });

        Register("wholesale", new OrderTemplate
        {
            CustomerType = "Wholesale",
            DiscountPercentage = 15,
            DefaultShipping = new ShippingOptions { Method = "Freight", RequireSignature = true }
        });

        Register("vip", new OrderTemplate
        {
            CustomerType = "VIP",
            DiscountPercentage = 25,
            DefaultShipping = new ShippingOptions { Method = "Express", RequireSignature = true }
        });
    }

    public void Register(string name, OrderTemplate template)
    {
        _templates[name.ToLowerInvariant()] = template;
        _logger.LogInformation("Registered order template: {TemplateName}", name);
    }

    public OrderTemplate? Get(string name)
    {
        return _templates.TryGetValue(name.ToLowerInvariant(), out var template)
            ? template
            : null;
    }

    public OrderTemplate CreateFrom(string templateName)
    {
        var template = Get(templateName)
            ?? throw new KeyNotFoundException($"Template '{templateName}' not found");

        _logger.LogDebug("Creating order from template: {TemplateName}", templateName);
        return template.CloneAsNew();
    }

    public IReadOnlyCollection<string> GetAvailableTemplates() => _templates.Keys.ToList();
}

// Usage with DI
public class OrderService
{
    private readonly OrderTemplateRegistry _templateRegistry;
    private readonly ILogger<OrderService> _logger;

    public OrderService(OrderTemplateRegistry templateRegistry, ILogger<OrderService> logger)
    {
        _templateRegistry = templateRegistry;
        _logger = logger;
    }

    public OrderTemplate CreateOrderForCustomer(string customerType, Action<OrderTemplate>? customize = null)
    {
        var order = _templateRegistry.CreateFrom(customerType);
        customize?.Invoke(order);

        _logger.LogInformation("Created order {OrderId} for {CustomerType}",
            order.TemplateId, order.CustomerType);

        return order;
    }
}

// DI Registration
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddOrderTemplates(this IServiceCollection services)
    {
        services.AddSingleton<OrderTemplateRegistry>();
        services.AddScoped<OrderService>();
        return services;
    }
}
❓ Interview Q&A

Q1: When is Prototype better than new? A1: When object creation is expensive (involves I/O, complex initialization), when you need many similar objects with slight variations, or when the exact type is determined at runtime.

Q2: How do you implement deep cloning in C#? A2: Options include: 1) Manual copying of all fields, 2) Serialization/deserialization (JSON, Binary), 3) Reflection-based copying, 4) Expression trees. JSON serialization is simplest for most cases.

Q3: What’s the difference between MemberwiseClone() and implementing ICloneable? A3: MemberwiseClone() creates a shallow copy. ICloneable.Clone() returns object and doesn’t specify deep vs shallowβ€”it’s poorly designed. Better to create your own IDeepCloneable<T> interface.


Singleton

Intent: Ensure a class has only one instance and provide a global point of access to it.

πŸ“– Theory & When to Use

When to Use

  • Exactly one instance is needed (configuration, logging, connection pool)
  • Controlled access to a sole instance is required
  • The single instance should be extensible by subclassing

When NOT to Use

  • In modern applications with DI containersβ€”prefer registering as singleton
  • When it hides dependencies (harder to test)
  • When global state can cause issues in parallel execution

Real-World Analogy

A government: a country has only one official government at a time, and everyone accesses it through the same channels.

Key Participants

  • Singleton: Class that manages its own unique instance
πŸ“Š UML Diagram
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          Singleton             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - instance: static Singleton   β”‚
β”‚ - data: SomeType               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ - Singleton()     // private   β”‚
β”‚ + Instance: static Singleton   β”‚
β”‚ + Operation()                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Short Example (~20 lines)
public sealed class Singleton
{
    private static readonly Lazy<Singleton> _instance =
        new(() => new Singleton());

    private Singleton() { }

    public static Singleton Instance => _instance.Value;

    public void DoSomething() => Console.WriteLine("Singleton operation");
}

// Usage
Singleton.Instance.DoSomething();
var s1 = Singleton.Instance;
var s2 = Singleton.Instance;
Console.WriteLine(s1 == s2); // True
πŸ’» Medium Example (~50 lines)
// Thread-safe singleton with configuration
public sealed class AppConfiguration
{
    private static readonly Lazy<AppConfiguration> _instance =
        new(() => new AppConfiguration(), LazyThreadSafetyMode.ExecutionAndPublication);

    private readonly Dictionary<string, string> _settings;

    private AppConfiguration()
    {
        // Simulate loading configuration
        _settings = new Dictionary<string, string>
        {
            ["ApiUrl"] = "https://api.example.com",
            ["Timeout"] = "30",
            ["MaxRetries"] = "3"
        };
    }

    public static AppConfiguration Instance => _instance.Value;

    public string Get(string key) =>
        _settings.TryGetValue(key, out var value) ? value : "";

    public T Get<T>(string key, T defaultValue = default!) where T : IParsable<T>
    {
        if (_settings.TryGetValue(key, out var value) &&
            T.TryParse(value, null, out var result))
        {
            return result;
        }
        return defaultValue;
    }

    public void Set(string key, string value)
    {
        lock (_settings)
        {
            _settings[key] = value;
        }
    }
}

// Usage
var apiUrl = AppConfiguration.Instance.Get("ApiUrl");
var timeout = AppConfiguration.Instance.Get<int>("Timeout", 60);
Console.WriteLine($"API: {apiUrl}, Timeout: {timeout}");

// Classic double-check locking (for reference)
public sealed class ClassicSingleton
{
    private static ClassicSingleton? _instance;
    private static readonly object _lock = new();

    private ClassicSingleton() { }

    public static ClassicSingleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    _instance ??= new ClassicSingleton();
                }
            }
            return _instance;
        }
    }
}
πŸ’» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;

// In modern .NET, prefer DI over traditional Singleton
// But here's a production-ready pattern when Singleton is truly needed

public interface IConnectionPool : IDisposable
{
    Task<IPooledConnection> AcquireAsync(CancellationToken ct = default);
    void Release(IPooledConnection connection);
    PoolStatistics GetStatistics();
}

public interface IPooledConnection : IAsyncDisposable
{
    string ConnectionId { get; }
    bool IsValid { get; }
    Task<T> ExecuteAsync<T>(Func<Task<T>> operation, CancellationToken ct = default);
}

public record PoolStatistics(int TotalConnections, int AvailableConnections, int InUseConnections);

// Singleton connection pool with proper resource management
public sealed class ConnectionPool : IConnectionPool
{
    private static readonly Lazy<ConnectionPool> _instance =
        new(() => new ConnectionPool(ConnectionPoolOptions.Default),
            LazyThreadSafetyMode.ExecutionAndPublication);

    public static ConnectionPool Instance => _instance.Value;

    private readonly ConcurrentBag<PooledConnection> _available = new();
    private readonly ConcurrentDictionary<string, PooledConnection> _inUse = new();
    private readonly SemaphoreSlim _semaphore;
    private readonly ConnectionPoolOptions _options;
    private readonly ILogger<ConnectionPool>? _logger;
    private bool _disposed;
    private int _totalCreated;

    // Private constructor for singleton
    private ConnectionPool(ConnectionPoolOptions options, ILogger<ConnectionPool>? logger = null)
    {
        _options = options ?? throw new ArgumentNullException(nameof(options));
        _logger = logger;
        _semaphore = new SemaphoreSlim(options.MaxConnections, options.MaxConnections);
    }

    // Factory method for DI scenarios (preferred in modern apps)
    public static ConnectionPool Create(ConnectionPoolOptions options, ILogger<ConnectionPool>? logger = null)
    {
        return new ConnectionPool(options, logger);
    }

    public async Task<IPooledConnection> AcquireAsync(CancellationToken ct = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);

        if (!await _semaphore.WaitAsync(_options.AcquireTimeout, ct))
        {
            throw new TimeoutException("Could not acquire connection from pool within timeout");
        }

        try
        {
            if (_available.TryTake(out var connection) && connection.IsValid)
            {
                _inUse[connection.ConnectionId] = connection;
                _logger?.LogDebug("Reused connection {ConnectionId}", connection.ConnectionId);
                return connection;
            }

            // Create new connection
            var newConnection = await CreateConnectionAsync(ct);
            _inUse[newConnection.ConnectionId] = newConnection;
            Interlocked.Increment(ref _totalCreated);
            _logger?.LogDebug("Created new connection {ConnectionId}", newConnection.ConnectionId);
            return newConnection;
        }
        catch
        {
            _semaphore.Release();
            throw;
        }
    }

    public void Release(IPooledConnection connection)
    {
        if (connection is not PooledConnection pooled) return;

        if (_inUse.TryRemove(pooled.ConnectionId, out _))
        {
            if (pooled.IsValid && !_disposed)
            {
                _available.Add(pooled);
                _logger?.LogDebug("Released connection {ConnectionId} back to pool", pooled.ConnectionId);
            }
            else
            {
                pooled.Dispose();
                _logger?.LogDebug("Disposed invalid connection {ConnectionId}", pooled.ConnectionId);
            }
            _semaphore.Release();
        }
    }

    public PoolStatistics GetStatistics()
    {
        return new PoolStatistics(
            TotalConnections: _totalCreated,
            AvailableConnections: _available.Count,
            InUseConnections: _inUse.Count
        );
    }

    private async Task<PooledConnection> CreateConnectionAsync(CancellationToken ct)
    {
        await Task.Delay(10, ct); // Simulate connection setup
        return new PooledConnection(this);
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        foreach (var conn in _available)
            conn.Dispose();

        foreach (var conn in _inUse.Values)
            conn.Dispose();

        _semaphore.Dispose();
        _logger?.LogInformation("Connection pool disposed. Total connections created: {Total}", _totalCreated);
    }
}

public class PooledConnection : IPooledConnection, IDisposable
{
    private readonly ConnectionPool _pool;
    private bool _disposed;

    public string ConnectionId { get; } = Guid.NewGuid().ToString("N")[..8];
    public bool IsValid => !_disposed;

    internal PooledConnection(ConnectionPool pool) => _pool = pool;

    public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation, CancellationToken ct = default)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        return await operation();
    }

    public async ValueTask DisposeAsync()
    {
        _pool.Release(this);
        await ValueTask.CompletedTask;
    }

    internal void Dispose() => _disposed = true;
    void IDisposable.Dispose() => _pool.Release(this);
}

public record ConnectionPoolOptions
{
    public int MaxConnections { get; init; } = 100;
    public TimeSpan AcquireTimeout { get; init; } = TimeSpan.FromSeconds(30);
    public TimeSpan ConnectionLifetime { get; init; } = TimeSpan.FromMinutes(30);

    public static ConnectionPoolOptions Default => new();
}

// DI Registration (modern approach - register singleton in container)
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddConnectionPool(
        this IServiceCollection services,
        Action<ConnectionPoolOptions>? configure = null)
    {
        var options = new ConnectionPoolOptions();
        configure?.Invoke(options);

        services.AddSingleton<IConnectionPool>(sp =>
        {
            var logger = sp.GetService<ILogger<ConnectionPool>>();
            return ConnectionPool.Create(options, logger);
        });

        return services;
    }
}

// Usage
public class DataService
{
    private readonly IConnectionPool _pool;

    public DataService(IConnectionPool pool) => _pool = pool;

    public async Task<string> GetDataAsync(CancellationToken ct = default)
    {
        await using var conn = await _pool.AcquireAsync(ct);
        return await conn.ExecuteAsync(async () =>
        {
            await Task.Delay(100, ct);
            return "Data from database";
        }, ct);
    }
}
❓ Interview Q&A

Q1: Why is Singleton considered an anti-pattern by some? A1: It introduces global state, hides dependencies, makes testing difficult, and can cause issues in multi-threaded/distributed environments. Modern apps prefer DI containers to manage singleton lifetime.

Q2: How do you make Singleton thread-safe in C#? A2: Best approach: use Lazy<T> with LazyThreadSafetyMode.ExecutionAndPublication. Alternatives: static initializer, double-check locking with volatile, or Interlocked.CompareExchange.

Q3: How do you unit test code that uses Singleton? A3: Extract an interface, inject the singleton through DI, and mock the interface in tests. Or use a β€œresettable” singleton for testing (add internal Reset() method with [InternalsVisibleTo]).

Q4: What’s the difference between Singleton and static class? A4: Singleton can implement interfaces, be passed as parameters, use inheritance, and have instance state. Static classes are simpler but less flexible. Singleton can be lazy-initialized.


Quick Reference

Pattern Use Case Key Benefit
Factory Method Create objects without specifying exact class Decouples creation from usage
Abstract Factory Create families of related objects Ensures compatible products
Builder Construct complex objects step by step Readable, configurable construction
Prototype Clone existing objects Avoids expensive creation
Singleton Single instance needed Controlled global access

See Also

  • Structural Patterns - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy
  • Behavioral Patterns - Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor