Singleton Pattern
Control construction and expose a shared instance, with explicit limits on what single means.
· Last updated: 2026-10-25 · 5 min read
Overview
Several parts of a program use the same read-only catalog. Instead of constructing it repeatedly, the program can restrict construction and expose one shared object.
Singleton provides a single instance and an access point to it. Here the constructor is private, and callers obtain the object through Instance.
UML
Callers use AppCatalog.Instance. Its constructor is private, and a static Lazy<AppCatalog> creates the object on first access.
History
GoF cataloged Singleton as a creational pattern in Design Patterns in 1994. It restricts a class to one instance and provides access to it.1
Why use it?
One creation point can suit a shared read-only catalog. Global access also hides a dependency; explicitly creating and injecting an object is easier when tests or callers need different instances.
The scope here is the static field in the same runtime load context. Multiple server processes each have their own instance. This is not a distributed mechanism for preventing duplicate work.
Example
Check whether two accesses return the same object reference.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(ReferenceEquals(AppCatalog.Instance, AppCatalog.Instance));
// Own construction so callers do not create separate catalogs.
sealed class AppCatalog
{
private static readonly Lazy<AppCatalog> shared = new(() => new AppCatalog());
private AppCatalog() { }
// Create on first synchronous access and reuse the object within this module instance.
public static AppCatalog Instance => shared.Value;
public string Version => "v1";
}
TrueLazy<T> synchronizes initialization. That does not automatically make mutable operations added to the object thread-safe.
An extended example
Have several tasks access the instance concurrently. The example catalog remains read-only.
using System;
using System.Collections.Generic;
using System.Linq;
var tasks = Enumerable.Range(0, 8)
.Select(_ => System.Threading.Tasks.Task.Run(() => AppCatalog.Instance)).ToArray();
var values = await System.Threading.Tasks.Task.WhenAll(tasks);
Console.WriteLine(values.All(value => ReferenceEquals(value, values[0])));
Console.WriteLine(values[0].Version);
// Own construction so callers do not create separate catalogs.
sealed class AppCatalog
{
private static readonly Lazy<AppCatalog> shared = new(() => new AppCatalog());
private AppCatalog() { }
// Create on first synchronous access and reuse the object within this module instance.
public static AppCatalog Instance => shared.Value;
public string Version => "v1";
}
True
v1These calls share one static field and receive one instance. If configuration must vary by user, first check whether that sharing scope fits the application.
TypeScript example
AppCatalog keeps construction and access in one place so callers share the same catalog. They obtain instance instead of constructing another catalog.
// Own construction so callers do not create separate catalogs.
class AppCatalog {
static #shared: AppCatalog | undefined;
readonly version = "v1";
private constructor() {}
// Create on first synchronous access and reuse the object within this module instance.
static get instance(): AppCatalog {
return AppCatalog.#shared ??= new AppCatalog();
}
}
const first = AppCatalog.instance;
const second = AppCatalog.instance;
console.log(first === second);
console.log(first.version);true
v1The sharing scope is the same JavaScript module instance. A TypeScript private constructor is a type-checking restriction, not a security boundary against JavaScript callers.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Creational patterns, Singleton. ↩