A logbook for curious waters
← Software Engineering Field Guide

Hidden Global State

Before you begin: Functions, variables, and objects.

Hidden global state is shared, changeable data that a function uses without receiving it as an argument or an explicit dependency. A call can produce a different answer because unrelated code changed that data earlier.

The word hidden does not mean the variable is impossible to find. It means the dependency is absent from the call or the object’s construction. A reader has to discover it by inspecting the implementation. Supplying dependencies explicitly is one way to remove that surprise. 1

Original version: one setting changes every greeting

A small game has a configurable greeting prefix. Two screens want different wording, but both read one shared setting.

public static class Settings
{
    public static string Prefix = "Hello";
}

public static class Greeting
{
    public static string For(string name)
    {
        return Settings.Prefix + ", " + name;
    }
}
Settings.Prefix = "Welcome";
Console.WriteLine(Greeting.For("Mira")); // Welcome, Mira
Settings.Prefix = "Ahoy";
Console.WriteLine(Greeting.For("Mira")); // Ahoy, Mira

For receives the same name twice, but it reads a different Settings.Prefix. Any code allowed to assign that static field can alter future greetings. A test that forgets to restore the field can affect another test.

prefix = "Hello"


def greeting(name):
    return prefix + ", " + name
prefix = "Welcome"
print(greeting("Mira"))  # Welcome, Mira
prefix = "Ahoy"
print(greeting("Mira"))  # Ahoy, Mira

greeting receives the same name twice, but reads a different module-level prefix. Code assigning that variable alters future greetings. A test that forgets to restore it can affect another test.

Revised version: each greeting receives its own setting

public sealed class Greeting
{
    private readonly string prefix;

    public Greeting(string prefix)
    {
        this.prefix = prefix;
    }

    public string For(string name)
    {
        return prefix + ", " + name;
    }
}
Greeting lobby = new Greeting("Welcome");
Greeting harbor = new Greeting("Ahoy");
Console.WriteLine(lobby.For("Mira"));
Console.WriteLine(harbor.For("Mira"));

Each object stores the prefix supplied to its constructor. Creating harbor does not change lobby. The string field cannot be reassigned after construction.

class Greeting:
    def __init__(self, prefix):
        self._prefix = prefix

    def for_player(self, name):
        return self._prefix + ", " + name
lobby = Greeting("Welcome")
harbor = Greeting("Ahoy")
print(lobby.for_player("Mira"))
print(harbor.for_player("Mira"))

Each object stores the prefix supplied to its constructor. Creating harbor does not change lobby. Python still permits deliberate assignment to _prefix; the underscore is a convention, not enforced immutability.

Make the lifetime decision explicit

The two designs differ intentionally: the original observes later global edits, while the revision keeps a per-object setting. If live configuration changes are required, inject a configuration provider and document that behavior instead of freezing a value accidentally.

Not all global data is troublesome. An immutable lookup table has no mutation history to discover. A process-wide service can be legitimate too, but its lifetime does not excuse making dependencies invisible.

Start with the practical symptom: a test passes alone and fails after another test, or two screens cannot choose different settings. Find the shared writes. Then decide whether the value should be an argument, an object-owned setting, or an explicitly shared service.

Sources

  1. Microsoft — Dependency injection guidelines, on stateful static members, service lifetimes, and explicit dependencies.