Loading content...
Singleton lifetime is conceptually the simplest: the container creates exactly one instance, and that instance serves all requests for the entire application lifetime. First request? Creates the instance. Subsequent requests? Returns the same instance. Application shutdown? Instance is finally disposed.
This simplicity is deceptive. While singleton semantics are easy to understand, the implications are profound:
Singleton lifetime is the right choice for genuinely global, stateless, or carefully synchronized services. It's the wrong choice for almost everything else. This page teaches you to distinguish between the two.
By the end of this page, you will understand singleton lifetime at a mastery level: its thread safety implications, common use cases like configuration and logging, the difference between DI singletons and the Singleton pattern, lazy vs eager initialization, and the subtle bugs that arise from incorrectly singleton-scoped services.
A singleton service in DI terminology is one where:
12345678910111213141516171819202122232425262728293031
// In .NET Core / .NET 5+public void ConfigureServices(IServiceCollection services){ // Type-based singleton registration services.AddSingleton<IConfigurationService, ConfigurationService>(); // Register existing instance as singleton var settings = new AppSettings { MaxRetries = 3, Timeout = 30 }; services.AddSingleton(settings); // Interface pointing to existing instance services.AddSingleton<IAppSettings>(settings); // Factory-based singleton with lazy initialization services.AddSingleton<IExpensiveService>(provider => { var config = provider.GetRequiredService<IConfiguration>(); var logger = provider.GetRequiredService<ILogger<ExpensiveService>>(); // This factory runs only once, on first resolution logger.LogInformation("Initializing ExpensiveService..."); return new ExpensiveService(config["ServiceEndpoint"]); }); // HttpClient factory (recommended pattern for HTTP) services.AddHttpClient<IApiClient, ApiClient>(client => { client.BaseAddress = new Uri("https://api.example.com/"); client.Timeout = TimeSpan.FromSeconds(30); });}1234567891011121314151617181920212223242526272829303132
// In Spring, singleton is the DEFAULT scope@Configurationpublic class AppConfig { // Singleton by default - no annotation needed @Bean public ConfigurationService configurationService() { return new ConfigurationService(); } // Explicit singleton annotation (same as default) @Bean @Scope("singleton") public CacheManager cacheManager() { return new ConcurrentMapCacheManager(); }} // Component scanning also creates singletons by default@Service // Implicitly singletonpublic class OrderProcessingService { private final PaymentGateway paymentGateway; @Autowired public OrderProcessingService(PaymentGateway paymentGateway) { this.paymentGateway = paymentGateway; System.out.println("OrderProcessingService singleton created"); }} // This class must be thread-safe!// All controller requests share the same instance123456789101112131415161718192021222324252627282930313233343536373839404142
// In InversifyJS - explicit singleton bindingimport { Container } from "inversify"; const container = new Container(); // Singleton scope - one instance for container lifetimecontainer.bind<IConfigService>(TYPES.ConfigService) .to(ConfigService) .inSingletonScope(); // Verify singleton behaviorconst config1 = container.get<IConfigService>(TYPES.ConfigService);const config2 = container.get<IConfigService>(TYPES.ConfigService);console.log(config1 === config2); // true - same instance // In NestJS - DEFAULT scope is singleton@Injectable() // Scope.DEFAULT = singletonexport class ConfigService { private readonly settings: Map<string, string>; constructor() { console.log('ConfigService singleton created'); this.settings = new Map(); this.loadSettings(); } get(key: string): string | undefined { return this.settings.get(key); } private loadSettings(): void { // Load from environment, files, etc. this.settings.set('API_URL', process.env.API_URL || ''); }} // Explicit singleton for emphasis@Injectable({ scope: Scope.DEFAULT })export class CacheService { private readonly cache = new Map<string, CacheEntry>(); // Implementation...}When you register a service as singleton, you're making a contract with the runtime: this service will be accessed by multiple threads simultaneously, and it must handle this correctly.
In a web application, each incoming HTTP request typically runs on a different thread (from the thread pool). If 100 requests arrive concurrently and all need your singleton service, 100 threads will call methods on the same instance at the same time.
This leads to the fundamental rule of singletons:
A singleton service MUST be either: (1) Completely stateless — no instance fields, or all fields are readonly/immutable, or (2) Fully thread-safe — all mutable state protected by locks, concurrent collections, or atomic operations. There is no middle ground. A singleton with unprotected mutable state is a data corruption bug waiting to happen.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
// ✅ SAFE: Completely stateless singletonpublic class PasswordHasher : IPasswordHasher{ // No instance state at all public string Hash(string password) { using var sha = SHA256.Create(); var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(password)); return Convert.ToBase64String(bytes); } public bool Verify(string password, string hash) { return Hash(password) == hash; }} // ✅ SAFE: Immutable state onlypublic class ConfigurationService : IConfigurationService{ // Readonly field, set once at construction private readonly ImmutableDictionary<string, string> _settings; public ConfigurationService(IConfiguration config) { _settings = config.AsEnumerable() .Where(kvp => kvp.Value != null) .ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value!); } public string GetSetting(string key) => _settings.TryGetValue(key, out var value) ? value : string.Empty;} // ✅ SAFE: Thread-safe collectionpublic class InMemoryCache : ICache{ // ConcurrentDictionary handles thread safety internally private readonly ConcurrentDictionary<string, CacheEntry> _cache = new(); public void Set(string key, object value, TimeSpan expiry) { _cache[key] = new CacheEntry(value, DateTime.UtcNow.Add(expiry)); } public T? Get<T>(string key) { if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTime.UtcNow) { return (T)entry.Value; } return default; }} // ❌ BROKEN: Unprotected mutable statepublic class BrokenCounterService : ICounterService{ private int _count = 0; // ⚠️ RACE CONDITION! public int Increment() { _count++; // NOT ATOMIC! Read-modify-write = race condition return _count; } // With 1000 concurrent calls, final count might be 950, 980, or 1000 // depending on thread timing - data corruption} // ✅ SAFE: Atomic operationspublic class SafeCounterService : ICounterService{ private int _count = 0; public int Increment() { return Interlocked.Increment(ref _count); // Atomic increment }} // ❌ BROKEN: Regular Dictionary (not thread-safe)public class BrokenCacheService : ICache{ private readonly Dictionary<string, object> _cache = new(); // ⚠️ public void Set(string key, object value) { _cache[key] = value; // Concurrent writes can corrupt internal state } // Runtime: Dictionary throws, loops forever, or returns garbage}| Strategy | How It Works | Best For |
|---|---|---|
| Stateless | No mutable instance fields | Pure functions, utilities |
| Immutable state | State set once at construction, never modified | Configuration, mappings |
| Concurrent collections | ConcurrentDictionary, ConcurrentQueue, etc. | Caches, queues |
| Atomic operations | Interlocked.Increment, etc. | Counters, flags |
| Lock-based | lock() or ReaderWriterLockSlim | Complex shared state |
| Actor model | Single thread processes messages | Complex state machines |
Singleton initialization can happen at two different times:
Lazy Initialization (Default): The singleton is created when first requested. If no code ever requests it, it's never created. This is the default behavior in most DI containers.
Eager Initialization: The singleton is created at application startup, before any requests arrive. This is useful for services that must be ready immediately or that should fail-fast if misconfigured.
The choice affects application startup time, failure behavior, and resource usage.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Default: Lazy initializationservices.AddSingleton<IMyService, MyService>();// MyService created when first injected or resolved // Force eager initialization: Resolve at startuppublic void Configure(IApplicationBuilder app, IServiceProvider services){ // Force immediate creation by resolving at startup var myService = services.GetRequiredService<IMyService>(); // Or use a dedicated method if you have many EagerlyInitializeSingletons(services);} private void EagerlyInitializeSingletons(IServiceProvider services){ // These are all created NOW, before first request services.GetRequiredService<ICacheWarmer>(); services.GetRequiredService<IFeatureFlagService>(); services.GetRequiredService<IMetricsCollector>();} // Pattern: Self-initializing singleton with fail-fastpublic class FeatureFlagService : IFeatureFlagService{ private readonly ImmutableDictionary<string, bool> _flags; public FeatureFlagService(IConfiguration config, ILogger<FeatureFlagService> logger) { logger.LogInformation("Loading feature flags..."); try { // If this fails, we want to know at startup, not on first use _flags = LoadFlagsFromRemoteService(config["FeatureService:Url"]); logger.LogInformation("Loaded {Count} feature flags", _flags.Count); } catch (Exception ex) { // Fail fast: application should not start with broken features logger.LogCritical(ex, "Failed to load feature flags"); throw; } }} // In Spring: Eager initialization is default for singletons@Componentpublic class StartupService implements InitializingBean { @Override public void afterPropertiesSet() throws Exception { // Called at startup, not lazily System.out.println("StartupService initialized"); }} // Explicit lazy in Spring@Component@Lazypublic class LazyService { // Created only when first injected}Use eager initialization for critical infrastructure: database connection pools, cache warmers, configuration loaders, feature flag providers. These services should fail-fast at startup rather than causing mysterious failures minutes later when the first request hits a broken service. The few seconds of extra startup time is worth the operational safety.
Singleton lifetime is appropriate for a well-defined category of services. These share common characteristics: they have no per-request state, they benefit from single initialization, or they explicitly manage shared resources.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// ✅ Configuration Service: Immutable after constructionpublic class AppConfiguration : IAppConfiguration{ public string DatabaseConnection { get; } public string ApiKey { get; } public int MaxRetryAttempts { get; } public TimeSpan RequestTimeout { get; } public AppConfiguration(IConfiguration config) { // Read at construction, immutable thereafter DatabaseConnection = config.GetConnectionString("Default") ?? throw new InvalidOperationException("Missing connection string"); ApiKey = config["ExternalApi:Key"] ?? throw new InvalidOperationException("Missing API key"); MaxRetryAttempts = config.GetValue<int>("Resilience:MaxRetries", 3); RequestTimeout = config.GetValue<TimeSpan>("Resilience:Timeout", TimeSpan.FromSeconds(30)); }} // ✅ In-Memory Cache: Thread-safe with concurrent collectionspublic class ProductCache : IProductCache{ private readonly ConcurrentDictionary<int, Product> _cache = new(); private readonly IProductRepository _repository; public ProductCache(IProductRepository repository) { _repository = repository; } public async Task<Product> GetOrLoadAsync(int productId) { // GetOrAdd is atomic - prevents duplicate loading if (_cache.TryGetValue(productId, out var cached)) { return cached; } // Load from database (only one thread will do this per key) var product = await _repository.GetByIdAsync(productId); _cache.TryAdd(productId, product); return product; } public void InvalidateProduct(int productId) { _cache.TryRemove(productId, out _); }} // ✅ Metrics Collector: Atomic operations for counterspublic class RequestMetricsCollector : IRequestMetrics{ private long _totalRequests = 0; private long _failedRequests = 0; private long _totalLatencyMs = 0; public void RecordRequest(long latencyMs, bool success) { Interlocked.Increment(ref _totalRequests); Interlocked.Add(ref _totalLatencyMs, latencyMs); if (!success) { Interlocked.Increment(ref _failedRequests); } } public MetricsSnapshot GetSnapshot() { var total = Interlocked.Read(ref _totalRequests); var failed = Interlocked.Read(ref _failedRequests); var latency = Interlocked.Read(ref _totalLatencyMs); return new MetricsSnapshot { TotalRequests = total, FailedRequests = failed, AverageLatencyMs = total > 0 ? (double)latency / total : 0 }; }} // ✅ HttpClient Factory: Proper singleton pattern for HTTP// In .NET, use IHttpClientFactory, not singleton HttpClientservices.AddHttpClient<IPaymentGateway, PaymentGateway>(client =>{ client.BaseAddress = new Uri("https://payments.example.com"); client.DefaultRequestHeaders.Add("X-Api-Version", "2.0");}).AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1)));Just as there are clear cases for singleton lifetime, there are clear cases against. These anti-patterns lead to bugs, poor testability, and maintenance nightmares:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
// ❌ WRONG: Per-request data in singletonpublic class UserContextService : IUserContext{ public int CurrentUserId { get; set; } // ⚠️ SHARED ACROSS REQUESTS! public void SetUser(int userId) { CurrentUserId = userId; // Request A sets to 100 // Request B sets to 200 // Request A reads... 200! Wrong user! }}services.AddSingleton<IUserContext, UserContextService>(); // ❌ // ✅ FIX: Use scoped lifetime for per-request dataservices.AddScoped<IUserContext, UserContextService>(); // Each request gets own instance // ❌ WRONG: Singleton DbContextservices.AddSingleton<ApplicationDbContext>(); // ❌ NEVER DO THIS // ✅ FIX: Use scoped lifetimeservices.AddScoped<ApplicationDbContext>(); // ❌ WRONG: Singleton with unprotected mutable statepublic class SessionTracker : ISessionTracker{ private readonly Dictionary<string, Session> _sessions = new(); // ⚠️ NOT THREAD-SAFE public void AddSession(string sessionId, Session session) { _sessions[sessionId] = session; // Concurrent access corrupts internal state }}services.AddSingleton<ISessionTracker, SessionTracker>(); // ❌ // ✅ FIX: Use concurrent collectionpublic class SafeSessionTracker : ISessionTracker{ private readonly ConcurrentDictionary<string, Session> _sessions = new(); public void AddSession(string sessionId, Session session) { _sessions[sessionId] = session; // Thread-safe }} // ❌ WRONG: Singleton capturing scoped dependencypublic class CachingOrderService : IOrderService{ private readonly ApplicationDbContext _db; // ⚠️ SCOPED! Captured by singleton! public CachingOrderService(ApplicationDbContext db) { _db = db; // This DbContext lives forever now }}services.AddSingleton<IOrderService, CachingOrderService>(); // ❌services.AddScoped<ApplicationDbContext>(); // This becomes captive // ✅ FIX: Inject scope factory for on-demand scoped resolutionpublic class SafeCachingOrderService : IOrderService{ private readonly IServiceScopeFactory _scopeFactory; public SafeCachingOrderService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task<Order?> GetOrderAsync(int id) { await using var scope = _scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); return await db.Orders.FindAsync(id); }}services.AddSingleton<IOrderService, SafeCachingOrderService>(); // ✅It's crucial to distinguish between singleton lifetime in DI and the Singleton design pattern. They share a name and a goal (single instance), but they differ fundamentally in implementation and implications:
| Aspect | DI Singleton Lifetime | Singleton Pattern |
|---|---|---|
| Instance creation | Container creates; class is unaware | Class controls its own instantiation |
| Access pattern | Injected as dependency | Static Instance property |
| Testability | Easily mocked via interface | Difficult to mock; requires workarounds |
| Coupling | Loose; depends on abstraction | Tight; depends on concrete class |
| Configuration | Composition root | Hardcoded in class |
| Thread safety | Container ensures single creation | Class must implement double-checked locking |
| Scope | Per container (can have multiple containers) | Per AppDomain/process (truly global) |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
// ❌ Classic Singleton Pattern - avoid in modern applicationspublic class LegacyConfigurationManager{ private static LegacyConfigurationManager? _instance; private static readonly object _lock = new(); public string DatabaseConnection { get; } private LegacyConfigurationManager() { DatabaseConnection = LoadFromFile("config.json"); } public static LegacyConfigurationManager Instance { get { if (_instance == null) { lock (_lock) { if (_instance == null) { _instance = new LegacyConfigurationManager(); } } } return _instance; } }} // Usage - tightly coupled, hard to testpublic class OrderService{ public void ProcessOrder(Order order) { // Direct dependency on static instance var connString = LegacyConfigurationManager.Instance.DatabaseConnection; // How do you test this with a different configuration? // How do you mock LegacyConfigurationManager? }} // ✅ DI Singleton - modern, testable approachpublic interface IConfigurationManager{ string DatabaseConnection { get; }} public class ConfigurationManager : IConfigurationManager{ public string DatabaseConnection { get; } public ConfigurationManager(IConfiguration config) { DatabaseConnection = config.GetConnectionString("Default")!; }} // Registrationservices.AddSingleton<IConfigurationManager, ConfigurationManager>(); // Usage - loosely coupled, easily testablepublic class OrderService{ private readonly IConfigurationManager _config; public OrderService(IConfigurationManager config) { _config = config; // Injected, mockable, testable } public void ProcessOrder(Order order) { var connString = _config.DatabaseConnection; }} // Testing is trivialpublic class OrderServiceTests{ [Fact] public void ProcessOrder_UsesConfiguration() { var mockConfig = new Mock<IConfigurationManager>(); mockConfig.Setup(c => c.DatabaseConnection).Returns("TestConnection"); var service = new OrderService(mockConfig.Object); // Test with controlled configuration }}Almost always prefer DI singleton lifetime over the Singleton pattern. DI singletons provide the same single-instance guarantee while maintaining testability, flexibility, and adherence to SOLID principles. The Singleton pattern is a legacy approach that creates hidden dependencies and testing obstacles.
Singleton services that hold resources (connections, file handles, timers) must implement proper disposal. The container disposes singletons when the container itself is disposed—typically at application shutdown.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
// Singleton with IDisposable for resource cleanuppublic class BackgroundTaskScheduler : ITaskScheduler, IDisposable{ private readonly Timer _timer; private readonly CancellationTokenSource _cts; private bool _disposed; public BackgroundTaskScheduler(ILogger<BackgroundTaskScheduler> logger) { _cts = new CancellationTokenSource(); _timer = new Timer(ExecutePendingTasks, null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); } private void ExecutePendingTasks(object? state) { if (_cts.IsCancellationRequested) return; // Process tasks... } public void Dispose() { if (!_disposed) { _cts.Cancel(); _timer.Dispose(); _cts.Dispose(); _disposed = true; } }} // For async cleanup, implement IAsyncDisposablepublic class ConnectionPool : IConnectionPool, IAsyncDisposable{ private readonly Channel<DbConnection> _pool; private readonly List<DbConnection> _allConnections = new(); public ConnectionPool(int poolSize, string connectionString) { _pool = Channel.CreateBounded<DbConnection>(poolSize); for (int i = 0; i < poolSize; i++) { var conn = new NpgsqlConnection(connectionString); conn.Open(); _allConnections.Add(conn); _pool.Writer.TryWrite(conn); } } public async ValueTask DisposeAsync() { _pool.Writer.Complete(); foreach (var conn in _allConnections) { await conn.CloseAsync(); await conn.DisposeAsync(); } }} services.AddSingleton<ITaskScheduler, BackgroundTaskScheduler>();services.AddSingleton<IConnectionPool, ConnectionPool>(); // Container disposal happens at shutdownpublic class Program{ public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build(); try { await host.RunAsync(); } finally { // Host disposal triggers singleton disposal await host.DisposeAsync(); // BackgroundTaskScheduler.Dispose() called // ConnectionPool.DisposeAsync() called } }}Singletons are disposed in reverse order of creation. If Singleton A depends on Singleton B, B was created first (to inject into A), so B is disposed last. This usually works correctly, but circular singleton dependencies can cause disposal issues. Avoid circular dependencies between singletons.
Singleton lifetime means one instance for the entire application. This provides maximum reuse but requires thread safety and careful design. Singletons are ideal for stateless utilities, caches, and infrastructure—but dangerous for anything with per-request state.
What's next:
You now understand all three lifetime strategies: transient, scoped, and singleton. The final piece is knowing how to choose between them. The next page provides a systematic decision framework for selecting the appropriate lifetime for any service.
You now understand singleton lifetime comprehensively: thread safety requirements, canonical use cases, anti-patterns, the difference from the Singleton pattern, and resource disposal. Next, we'll synthesize everything into a decision framework for choosing lifetimes.