The Cost of Global State
Global state feels convenient because it makes data and behaviour easy to reach. The trouble is that it also makes dependencies easy to hide, and hidden dependencies are where some of the most stubborn problems in Delphi applications begin.
Almost every Delphi developer has used global state.
Sometimes it is a global form variable.
Sometimes it is a shared data module.
Sometimes it is an application settings object, a singleton manager, a global cache, a global current document, or a unit full of variables and helper functions that quietly become part of the application's nervous system.
None of this is unusual.
In fact, Delphi has historically made some of it feel completely natural.
A form exists globally, so why not call it?
A data module is already there, so why not use it everywhere?
A setting is needed in twenty places, so why not keep it in one global object?
The appeal is obvious.
Global state reduces friction.
You do not need to pass objects around.
You do not need to think as hard about ownership.
You do not need to model dependencies explicitly.
And that is exactly the problem.
Global state makes dependencies easy to write and hard to see.
What Global State Really Means
Global state is any state or behaviour that can be reached from many parts of the program without being passed in explicitly.
That includes obvious examples such as:
- global variables;
- global form variables;
- shared data modules;
- singletons;
- class variables used as application-wide state;
- unit-level state hidden behind helper functions.
It also includes less obvious patterns where code appears independent but quietly reaches into global objects to retrieve what it needs.
For example:
procedure ProcessCustomer;
begin
if formMain.AppSettings.EnableCustomerSync then
dmServer.SyncCustomer(formCustomer.CustomerId);
end;
This procedure takes no parameters.
It appears self-contained.
In reality it depends on:
formMainexisting;AppSettingsbeing initialised;dmServerexisting;formCustomerexisting;- the current customer state already being loaded into that form;
- all of those objects behaving as expected at the time the method runs.
The procedure is not simple.
Its dependencies are merely hidden.
Why Delphi Makes This So Tempting
Delphi's visual application model has always made it easy to think in terms of application-wide objects.
Forms are often auto-created.
Data modules sit at the centre of everything.
A control on one form can be reached from another with only a few keystrokes.
formMain.lblStatus.Caption := 'Loading...';
It works.
It is fast to write.
It can even feel elegant in the moment.
The problem is not that global access is always wrong.
The problem is that repeated convenience at a local level slowly creates entanglement at the application level.
One form updates another.
A background operation reaches into the UI.
Business logic reads values directly from controls.
A helper function quietly depends on global configuration.
A data module becomes the place where everything is available, so everything starts depending on it.
No single step looks disastrous.
The architecture just gradually loses its boundaries.
The Real Cost Is Not Convenience β It Is Coupling
Global state is often defended because it saves plumbing.
And that is true.
The price is paid elsewhere.
When code depends on global state, it becomes tightly coupled to the surrounding application environment.
A method that should only be concerned with processing a document now also depends on:
- application initialisation order;
- the lifetime of global objects;
- which form currently owns the relevant state;
- whether configuration has already been loaded;
- whether another part of the system has mutated the same global state unexpectedly.
This is the hidden cost.
The dependency is real whether or not it is visible in the method signature.
Hiding it does not remove it.
Invisible Dependencies Are Harder to Reason About
Suppose you see this:
procedure ExportJob(const Job: TJob; const Exporter: IExporter);
You can learn a lot from that signature.
The method needs a job.
It needs something capable of exporting.
Its dependencies are visible.
Now compare that with:
procedure ExportJob;
That may look simpler.
But if the method internally reaches into formMain, dmJobs, a global settings object and a global current-exporter,
it is not simpler at all.
It is merely less honest.
One of the biggest architectural benefits of passing dependencies explicitly is clarity.
The code tells the truth about what it needs.
Global State and Shared Mutation
There is another problem.
Global state is usually mutable.
That means many different parts of the application can read it, change it and make assumptions about it.
If a global flag, list, cache or current-object changes unexpectedly, the effects may appear far away from the code which made the change.
This leads to some of the most unpleasant bugs in mature applications:
- behaviour depending on the order of earlier operations;
- screens showing stale data because something else changed shared state;
- flags left behind in the wrong state after an exception;
- code that only works if certain forms have been opened first;
- subtle regressions caused by a global object being reused in a slightly different context.
These bugs often feel mysterious because cause and effect are separated.
The application is no longer passing information through clear boundaries.
It is sharing a pool of assumptions.
Global Forms Are a Special Kind of Trouble
Delphi's global form variables deserve special mention because they blur the line between the user interface and the rest of the application.
Consider:
formProgress.Show;
formProgress.SetMessage('Importing records...');
formCustomer.ReloadCustomer;
formMain.UpdateStatusBar;
Code like this spreads quickly because it is so convenient.
Unfortunately it means application logic is now directly manipulating windows, controls and presentation state all over the place.
The code performing the import is not just importing records any more.
It is also orchestrating specific forms, specific UI controls and specific assumptions about which windows exist.
That tightens coupling in two directions:
- business logic now depends on the UI;
- UI state becomes part of the application's operational state.
The result is software that is harder to test, harder to reuse and harder to change.
Shared Data Modules Can Become Hidden Operating Systems
Data modules are extremely useful.
They are also one of the easiest places for global state to accumulate respectability.
A shared data module can begin as a tidy container for a database connection and a few datasets.
Over time it gains:
- queries;
- transaction logic;
- application settings;
- cached values;
- utility methods;
- permission checks;
- business rules;
- reporting helpers;
- state flags used by half the application.
Eventually the application no longer has one dependency.
It has a dependency on everything inside that data module.
The object becomes less like a focused module and more like a small operating system that the rest of the code quietly relies upon.
Singletons Are Global State Wearing a Tie
Singletons are often introduced to make global access look more disciplined.
Instead of a plain global variable, we now have:
TAppSettings.Instance.ThemeName
Or:
TJobManager.CurrentJob
This can be neater than raw globals.
But architecturally it is still usually global state.
The dependency is still implicit.
The lifetime is still shared.
Mutation is still potentially visible everywhere.
The code may look cleaner, but the coupling remains.
A singleton can be perfectly acceptable for genuinely application-wide infrastructure in some cases.
The problem begins when it becomes the default answer to every dependency.
Testing Quickly Reveals the Cost
Testability is one of the clearest indicators that global state has become excessive.
Ask yourself:
Can I run this behaviour in isolation?
If testing a validation rule requires:
formMainto exist;- a settings singleton to be initialised;
- a global data module to connect successfully;
- a current job object to have been established elsewhere;
- and three state flags to be set correctly first;
then the software is telling you something important.
The problem is not just the test.
The problem is that the behaviour is inseparable from the wider application context.
That makes ordinary change harder as well.
Explicit Dependencies Look More Verbose β and That Is Fine
One reason global state remains attractive is that the alternative can look more verbose.
Compare:
ProcessOrder;
with:
ProcessOrder(Order, Settings, Repository, Notifier);
The second version undeniably involves more plumbing.
It also tells the truth.
The method depends on an order, settings, persistence and notification.
That information is useful.
Some architectural clarity is always paid for in a little structural effort.
That effort is often much cheaper than spending months maintaining code whose dependencies are invisible.
Passing Dependencies Does Not Mean Passing Everything Everywhere
The sensible response to global state is not to turn every method call into an absurd procession of parameters.
The real aim is to keep dependencies at appropriate boundaries.
Often that means giving a class the dependencies it needs when it is created:
constructor TJobExporter.Create(const Repository: IJobRepository;
const Settings: IExportSettings;
const Notifier: IUserNotifier);
Then the class can use those explicit dependencies internally without reaching into the application globally.
This is one of the practical benefits of constructor injection and interface-based design: dependencies remain visible, replaceable and testable.
Not All Shared State Is Evil
It is worth being reasonable here.
Some application state really is global in nature.
Theme settings, application-wide configuration, a logging sink, or a deliberately centralised service may be acceptable shared resources.
The key questions are:
- Is this state genuinely application-wide?
- Does this code really need direct access to it?
- Could the dependency be made clearer?
- Is the state mutable, and if so who is responsible for changing it?
- Will this make testing or future change substantially harder?
The danger is not the existence of any shared object.
The danger is allowing shared state to become the default mechanism through which unrelated parts of the system communicate.
A Useful Smell: Code That Needs the Application to Be βJust Soβ
One of the strongest warning signs is code that only works when the application has been used in a particular sequence.
For example:
- this function only works after the customer screen has been opened;
- that report only works if settings have already been loaded by the main form;
- this operation fails if the cache has not been populated by an earlier action;
- that dialog assumes the current document has already been selected elsewhere.
These are often symptoms of hidden shared state.
Behaviour depends not merely on inputs, but on the invisible historical condition of the program.
That is always harder to reason about.
How to Start Reducing Global State
Mature applications rarely eliminate global state in one heroic refactor.
Nor should they try.
A safer approach is gradual improvement.
Start by identifying code that reaches into global forms, data modules or singleton objects for behaviour that obviously does not belong to the UI.
Then ask:
What does this code really need?
Often the answer is surprisingly small.
A method reaching into five global objects may really need only:
- a customer ID;
- a repository;
- some settings;
- a notifier.
Introduce those dependencies explicitly.
Pass them in.
Move the behaviour into a class with a clear purpose.
Let the UI coordinate rather than own the logic.
Over time, the application's structure becomes more honest.
Global State Usually Solves Today's Problem by Creating Tomorrow's
This is the uncomfortable truth at the centre of the issue.
Global state often feels like the most efficient solution to the immediate problem.
It removes friction now.
The cost appears later:
- when requirements change;
- when code needs to be reused;
- when tests are written;
- when a new screen is added;
- when application startup order changes;
- when nobody can quite explain why a flag has the wrong value.
That is why global state is so persistent.
Its benefit is immediate and local.
Its cost is delayed and architectural.
Keep Dependencies Honest
Good architecture does not require every dependency to be abstract, injected or ceremonially wrapped.
But it does benefit enormously from honesty.
If a class depends on settings, say so.
If it depends on a repository, say so.
If it needs a notifier, say so.
Hidden dependencies are still dependencies.
They are simply harder to understand and harder to control.
Delphi makes global access wonderfully easy.
Use that convenience sparingly.
The less your code quietly relies on the invisible condition of the application around it, the easier it will be to change, test and trust.
If a dependency matters, let the code admit it.
Your future self will thank you.