Lesson 0019 · System Designer · Module 2
Identifying and Reducing Global Coupling
A module-level mutable value is a phone line to every file in the project. Global coupling is the quietest way a codebase learns to depend on call order.
Mission tie-in: "identify and reduce global coupling" — the System Designer's first complexity leak, and the one AI code most reliably introduces with caches and singletons.
Knowledge: the hidden phone line
Global state is any state reachable from anywhere: a module-level mutable container, a singleton, a cache, a registry. The coupling it creates is invisible — no import declares it, but every consumer shares it:
# config.py
_settings: dict = {} # module-level mutable state
def set_setting(key: str, value) -> None:
_settings[key] = value
def get_setting(key: str):
return _settings.get(key)
# service_a.py
import config
config.set_setting("rate_limit", 10) # runs at import time
# service_b.py
import config
limit = config.get_setting("rate_limit") # 10 — only if service_a imported first
The bug is in the wiring, not the values: service_b's behavior depends on service_a having run first. Import order, test order, and deployment order all become part of your program's semantics. Change a test file and the behavior changes — with no diff pointing at the cause.
The fix is to make the dependency explicit — pass it, own it, inject it:
class RateLimiter:
def __init__(self, limit: int) -> None:
self._limit = limit
self._used = 0
def allow(self) -> bool:
if self._used >= self._limit:
return False
self._used += 1
return True
# wiring happens once, visibly, at the composition root:
limiter = RateLimiter(limit=10)
service_b = ServiceB(limiter=limiter)
Now the dependency appears in the constructor — reviewable, mockable, and ordered by whoever builds the system, not by import order.
† The greedy-review test: grep for module-level mutable containers (= {}, = [], = 0 at module scope) and singletons. Every hit is a candidate phone line. Exceptions: constants (immutable) and caches whose invalidation you own consciously.
| You see this | What it costs | The move |
|---|---|---|
| _registry = {} at module scope | Behaviour depends on which module imported first. | Build the registry at the composition root. |
| @lru_cache on a config reader | The first call freezes the value for the whole process. | Cache on an object you can create and discard. |
| a test that passes alone, fails in the suite | State carried between tests by something shared. | Inject it; give every test its own instance. |
| os.environ[…] read at import time | Deployment config baked in before main() runs. | Read it at the composition root; pass the value down. |
Skill: find the phone lines
A module-level mutable dict read by two services creates:
The right home for a rate limit shared by two services is:
A module-level cache is acceptable when:
Practice on your own code
Grep your project (and AI output) for module-level mutable containers and singletons. Pick one that two or more modules read or write. Convert it to an owned, injected object and move construction to a composition root.
Reveal: a worked de-globalization
# before: settings._registry mutated at import time by several plugins
import settings
settings.register_plugin(MyPlugin()) # import side effects everywhere
# after: a PluginRegistry object built once, explicitly
registry = PluginRegistry()
registry.register(MyPlugin())
app = build_app(registry)
Plugin registration is now visible in the build code, testable in isolation, and immune to import order.
Your win
You can grep for global coupling, name the hidden dependency it creates (call order), and replace it with an injected, owned object — with the composition root as the one place wiring happens.
Read and watch deeper
- The Pragmatic Programmer, Hunt & Thomas — the sections on global data and "decoupling": the classic treatment.
- Code Complete, McConnell — ch. 5: global data as the worst coupling level (common coupling).
- A Philosophy of Software Design, Ousterhout — ch. 5 "Information Hiding": what the module exposes is a choice; globals expose everything.
- Watch: ArjanCodes YouTube — search "global state" or "dependency injection".
- Next: lesson 0020 — shared state and ownership.
- Reference: Design Principles — separate creation from use; glossary — global coupling, composition root.
Run the greedy-review grep with your agent-teacher and decide each hit together.