Singleton Pattern Guide
Comprehensive guide to using the Singleton pattern in PatternKit.
Overview
Singleton ensures a class has only one instance and provides a global point of access. This implementation adds fluent configuration, one-time initialization, and thread-safe lazy/eager creation.
flowchart LR
subgraph Singleton
F[Factory]
I[Init Actions]
V[(Instance)]
end
A[Access] --> C{Created?}
C -->|No| F
F --> I
I --> V
C -->|Yes| V
V --> R[Return]
Getting Started
Installation
using PatternKit.Creational.Singleton;
Basic Usage
// Create a lazy singleton
var cache = Singleton<Cache>
.Create(() => new Cache())
.Init(c => c.WarmUp())
.Build();
// Access the instance (created on first access)
var instance = cache.Instance;
Eager Creation
// Create immediately at Build() time
var config = Singleton<Configuration>
.Create(() => LoadConfiguration())
.Eager()
.Build(); // Instance already exists
Core Concepts
The Factory Delegate
You provide a Factory delegate that creates the singleton instance:
public delegate T Factory();
Common patterns:
// Simple construction
.Create(() => new Service())
// With dependencies
.Create(() => new Service(
connectionString,
logger))
// Static lambda to avoid closures
.Create(static () => new StatelessService())
Init Actions
One-time initialization runs after instance creation:
var db = Singleton<Database>
.Create(() => new Database(connectionString))
.Init(db => db.OpenConnection())
.Init(db => db.RunMigrations())
.Init(db => db.SeedData())
.Build();
Init actions compose in order and run exactly once.
Lazy vs Eager
Lazy (default): Instance created on first Instance access.
var singleton = Singleton<Service>
.Create(() => new Service())
.Build();
// Nothing created yet
// ...later...
var svc = singleton.Instance; // Created here
Eager: Instance created immediately at Build() time.
var singleton = Singleton<Service>
.Create(() => new Service())
.Eager()
.Build(); // Created here
var svc = singleton.Instance; // Same instance
Use eager when:
- Initialization must complete before application starts
- You want to fail fast on construction errors
- The cost of first-access delay is unacceptable
Common Patterns
Configuration Service
public static class AppConfig
{
private static readonly Singleton<Configuration> _instance =
Singleton<Configuration>
.Create(static () => new Configuration())
.Init(static c => c.LoadFromFile("appsettings.json"))
.Init(static c => c.LoadFromEnvironment())
.Init(static c => c.Validate())
.Eager() // Fail fast on invalid config
.Build();
public static Configuration Instance => _instance.Instance;
}
// Usage
var timeout = AppConfig.Instance.HttpTimeout;
Connection Pool
public class ConnectionPoolService
{
private readonly Singleton<ConnectionPool> _pool;
public ConnectionPoolService(string connectionString)
{
_pool = Singleton<ConnectionPool>
.Create(() => new ConnectionPool(connectionString, maxSize: 100))
.Init(p => p.WarmUp(minConnections: 10))
.Build();
}
public IDbConnection GetConnection() =>
_pool.Instance.Acquire();
}
Logger Factory
public static class Logging
{
private static readonly Singleton<ILoggerFactory> _factory =
Singleton<ILoggerFactory>
.Create(static () => LoggerFactory.Create(builder =>
{
builder
.SetMinimumLevel(LogLevel.Information)
.AddConsole()
.AddFile("logs/app.log");
}))
.Eager()
.Build();
public static ILogger<T> GetLogger<T>() =>
_factory.Instance.CreateLogger<T>();
}
// Usage
var logger = Logging.GetLogger<MyService>();
Feature Flags
public class FeatureFlags
{
private static readonly Singleton<FeatureFlags> _instance =
Singleton<FeatureFlags>
.Create(static () => new FeatureFlags())
.Init(static f => f.LoadFromConfig())
.Init(static f => f.SetupRefreshTimer())
.Build();
public static FeatureFlags Instance => _instance.Instance;
private Dictionary<string, bool> _flags = new();
private Timer? _refreshTimer;
private void LoadFromConfig()
{
// Load from config file or remote service
}
private void SetupRefreshTimer()
{
_refreshTimer = new Timer(_ => LoadFromConfig(),
null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public bool IsEnabled(string feature) =>
_flags.TryGetValue(feature, out var enabled) && enabled;
}
// Usage
if (FeatureFlags.Instance.IsEnabled("new-checkout"))
{
// New checkout flow
}
Metrics Registry
public class MetricsRegistry
{
private readonly Singleton<MeterProvider> _provider;
private readonly Dictionary<string, Counter<long>> _counters = new();
private readonly object _lock = new();
public MetricsRegistry(string serviceName)
{
_provider = Singleton<MeterProvider>
.Create(() => Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService(serviceName))
.AddPrometheusExporter()
.Build())
.Eager()
.Build();
}
public Counter<long> GetCounter(string name)
{
lock (_lock)
{
if (!_counters.TryGetValue(name, out var counter))
{
var meter = new Meter(name);
counter = meter.CreateCounter<long>(name);
_counters[name] = counter;
}
return counter;
}
}
}
Best Practices
Use Static Lambdas
Avoid closure allocations:
// Good - static lambda
.Create(static () => new Service())
.Init(static s => s.Initialize())
// Avoid - captures local variables
var config = LoadConfig();
.Create(() => new Service(config)) // Captures config
Keep Init Idempotent
For testability and safety:
.Init(static s =>
{
if (!s.IsInitialized)
s.Initialize();
})
Prefer Dependency Injection
Singletons can complicate testing. Consider:
// For DI containers
services.AddSingleton<IService>(sp =>
{
var singleton = Singleton<Service>
.Create(() => new Service(sp.GetRequiredService<ILogger>()))
.Init(s => s.Initialize())
.Build();
return singleton.Instance;
});
Handle Initialization Errors
var singleton = Singleton<Database>
.Create(() =>
{
try
{
return new Database(connectionString);
}
catch (Exception ex)
{
Log.Error("Failed to create database", ex);
throw;
}
})
.Init(db =>
{
if (!db.CanConnect())
throw new InvalidOperationException("Cannot connect to database");
})
.Eager() // Fail fast
.Build();
Thread Safety
| Component | Thread-Safe |
|---|---|
Builder |
No - single-threaded configuration |
Singleton<T> |
Yes - double-checked locking |
Instance |
Yes - volatile read with lock fallback |
| Init actions | Run exactly once, thread-safe |
Implementation Details
// Double-checked locking pattern
public T Instance => Volatile.Read(ref _created) ? _value : CreateSlow();
private T CreateSlow()
{
lock (_sync)
{
if (_created) return _value;
_value = _factory();
_init?.Invoke(_value);
Volatile.Write(ref _created, true);
return _value;
}
}
Troubleshooting
Initialization order issues
If singleton A depends on singleton B, ensure B is created first:
// Option 1: Eager creation in dependency order
var b = Singleton<B>.Create(...).Eager().Build();
var a = Singleton<A>.Create(() => new A(b.Instance)).Build();
// Option 2: Access in Init
var a = Singleton<A>
.Create(() => new A())
.Init(a => a.SetDependency(SingletonB.Instance))
.Build();
Testing with singletons
Use wrapper interfaces:
public interface IConfigProvider
{
Configuration Config { get; }
}
public class SingletonConfigProvider : IConfigProvider
{
public Configuration Config => ConfigSingleton.Instance;
}
// In tests
services.AddSingleton<IConfigProvider>(new MockConfigProvider());
Memory leaks with Init
If Init captures references, they live as long as the singleton:
// Potential issue - captures logger
var logger = GetLogger();
.Init(s => logger.Log("Initialized"))
// Better - resolve inside Init
.Init(s => GetLogger().Log("Initialized"))