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