Software Engineering DISCUSSION

If singletons and global state are bad design, how should shared services like config be provided?

Started by happisales singleton patternglobal statedependency injectioncomposition roottestability
5 replies 248 views 6 participants
Latest activity · 30 Sep 2026

If singletons and global state are bad design, how should shared services like config be provided?

happisales Software Engineering Forum
#1

My application has a Config singleton, a Logger singleton and a Database singleton, each reached through a static getInstance() from wherever it is needed. I keep reading that singletons and global state are harmful, but every part of the program genuinely needs these objects and there really should be only one of each.

What exactly is the harm if the design matches reality, and what is the alternative that does not involve passing ten parameters to every function?

Community replies 5

Re: If singletons and global state are bad design, how should shared services like config be provided?

#2

The problem is not that there is one instance. It is that the instance is reachable from everywhere. When any function can call Config.getInstance(), its signature no longer tells you what it depends on, so you cannot reason about or reuse it without reading its whole body and everything it calls. Initialisation order becomes implicit: something must have set up the database before the first caller arrives, and nothing in the code states that.

Mutable global state adds the worst part: any code can change it, so a bug's cause can be arbitrarily far from its symptom, and with threads every access is a potential race.

Re: If singletons and global state are bad design, how should shared services like config be provided?

#3

The alternative keeps the single instance and removes the global access. Create each object once, at program start, and hand it to the components that need it through their constructors: OrderService(database, clock, logger). The place where everything is created and wired together, typically main, is called the composition root. One instance is now a decision made in one place, not a property enforced by the class.

This is dependency injection, and it needs no framework. A container becomes worthwhile only when the wiring code itself grows large.

Re: If singletons and global state are bad design, how should shared services like config be provided?

#4

Testing is where the difference is most concrete. Suppose a rule says an invoice is overdue 30 days after issue. If the code reads the system time through a global, the test cannot control the date. If the class receives a clock object, the test passes a fake clock fixed at day 31 and the result is deterministic.

With singletons, tests also leak state into each other: one test changes a config value and a later test fails only when the two run in a certain order. Injected objects are created fresh per test, so that category of failure disappears.

Re: If singletons and global state are bad design, how should shared services like config be provided?

#5

On the fear of ten parameters: constructor injection puts dependencies on objects, not on every function call. A class takes its three collaborators once and its methods use them. If a constructor really does need eight or ten dependencies, that is a signal that the class has too many responsibilities, and a useful signal that the singleton version was hiding.

Also pass the narrowest thing that does the job. A function that needs a timeout should receive the timeout value, not the whole configuration object. It is then obvious what it uses, and testing it needs no configuration at all.

Re: If singletons and global state are bad design, how should shared services like config be provided?

#6

Two pragmatic notes. Logging is commonly treated as an acceptable exception: it is write-only, the program's behaviour does not depend on it, and threading a logger through every class adds noise, so a static logger per class or module configured in one place is normal practice in many ecosystems.

And you do not have to convert an existing codebase in one step. Leave getInstance() in place, change one class at a time to accept the dependency in its constructor, and let the call sites pass Config.getInstance() for now. The global calls migrate outward towards main until only the composition root is left using them.

TEP COMMUNITY