Singleton and object lifetime
Picture
A configuration value or connection pool may be shared. That is a lifetime decision, not proof every caller should fetch a global object.
Predict
Does one singleton in each of three server processes create one shared distributed instance?
Walk through the small example
No: there are three process-local instances. Distributed uniqueness requires coordination beyond a class-level variable.
Mental model and complete Python solution
Singleton restricts ordinary construction to a shared instance, but often introduces global access and difficult test isolation. ProcessSettings below returns the same object and initializes it while holding a lock. Application then demonstrates an alternative: a composition root constructs one immutable configuration and injects it into services. This preserves shared lifetime without enforcing global uniqueness. The contrast matters: the first controls construction globally; the second makes ownership explicit.
What the function promises: Ordinary ProcessSettings() calls share one initialized object in this Python interpreter. Subclassing is rejected. This is an application convention, not protection against deliberate constructor bypass, copying, serialization, or separate interpreters. Each Application instead owns a frozen Config and shares it with its own services; different Application objects remain isolated. No external resources require shutdown in this example.
from dataclasses import dataclass
from threading import Lock
class ProcessSettings:
_instance = None
_lock = Lock()
def __init_subclass__(cls, **kwargs):
raise TypeError("This singleton is not extensible")
def __new__(cls):
with cls._lock:
if cls._instance is None:
instance = super().__new__(cls)
instance._region = "default"
cls._instance = instance
return cls._instance
@property
def region(self):
return self._region
assert ProcessSettings() is ProcessSettings()
assert ProcessSettings().region == "default"
@dataclass(frozen=True)
class Config:
region: str
class Service:
def __init__(self, config):
self.config = config
class Application:
def __init__(self, region):
self.config = Config(region)
self.reader = Service(self.config)
self.writer = Service(self.config)
a, b = Application("test-a"), Application("test-b")
assert a.reader.config is a.writer.config
assert a.config is not b.config
assert b.reader.config.region == "test-b"
Work and storage: O(1) shared references. Real shared helpers such as pools have their own resource and shutdown costs.
When to recognize it: A language-level singleton is usually one per process, not one across every server. Hidden globals also make tests and replacement harder.
Check before applying it: Shared lifetime is not distributed uniqueness. Avoid hiding important dependencies behind globals.
Change one rule
We need one job scheduler leader across many machines. Does hiding its constructor solve that?
See what changes
No. Use a shared coordination protocol or service with leases/fencing or consensus-backed leadership as appropriate. Process-local object lifetime is a different problem.
Connect
Make failure and retries part of the design: A local lock coordinates one object, not a whole fleet of servers.
Monolith, services, and ownership: Deployment ownership is a different lifetime and coordination boundary.