10

I have a question regarding my general understanding of global state in software engineering.

When I write an app I like to decompose it into little, manageable components and functions, that are ideally self-contained, have their own local state and don't produce any side effect.

But some types of those functions seem to be inherently tied to some sort of global state, yet global state seems to be globally banned - excuse my pun - among software engineering folks. Usually they will tell you that "you can't reason about global state, that's why it's bad and you must not use it".

A primary example of an inherently global state of an application is the authentication status of a user. How can any solution that handles such global state possibly be inherently better than something like a global, public, static class or just global variables that can be accessed from anywhere in the app to check for the user data?

It just seem so convenient to have global variables for some types of data, yet they get so much hate and are always advised against. What is truly the underlying issue behind them other than "they can't be reasoned about" whatever that means? I know concurrent writes could be an issue but not all types of data are constantly or even concurrently written to?

7
  • 1
    Immutability solves most of the really nasty issues. There are others though. I would say the main one being that it makes a design hard to decompose i.e., it creates coupling across potentially unrelated parts of the design. I think it's a little overstated though. A lot of applications will never need to be decomposed in that way.
    – JimmyJames
    Commented Aug 17, 2022 at 19:02
  • 3
    "How can any solution that handles such global state possibly be inherently better than something like a global, public, static class or just global variables that can be accessed from anywhere in the app to check for the user data?" As a counter argument, I'd put forth the example of all the apps that didn't embrace the iPhone/iPads new-found ability to open multiple instances of one app at once. "There will only ever be one instance of my app, so global-scope == app scope". Welp, not anymore, and all those people's apps are incompatible without huge rework.
    – Alexander
    Commented Aug 17, 2022 at 19:52
  • 2
    @Alexander, yes, that is great fun. Lots of Singletons suddenly are not singletons anymore. Lots of assumptions breaking.
    – gnasher729
    Commented Aug 18, 2022 at 9:32
  • 1
    I had an application with an address book (truly global), a list of filtered addresses (by entering text into a search field, suddenly not global anymore), and a list of sorted addresses (suddenly not global anymore, because several apps can have different sorting). Fun.
    – gnasher729
    Commented Aug 18, 2022 at 9:39
  • Does this answer your question? Why is Global State so Evil?
    – gnat
    Commented Aug 18, 2022 at 10:46

7 Answers 7

11

I think you are conflating two (surely related) things here: global state, and global variables which are accessible from everywhere in a huge application. An application can have global state without global variables, but global variables almost inevitably introduce global-application wide state.

  • Global state becomes an issue especially when the state is mutable. For example, the authentication status of a user, or even worse, the connection status for a database application: when this state changes during the runtime of an application, all kind of unpredictable things can happen when an application does not have in-depth error handling at all kind of levels.

    Unfortunately, in a typical DB application, you cannot really avoid this global state to change from the outside - when someone pulls the network plug, the connection will be gone, and the state will change, if one likes it or not (for the sake of this example, put aside failover mechanisms which handle such issues in the background).

    However, something like the authentication status might be set only at the start of an application, and kept as immutable during the rest of the application's lifetime. Such (almost immutable) global state is way easier to deal with, since large parts of the code are usually not affected by it.

    You find more information about this topic in this older SE.SE question: Why is Global State so Evil?

  • Global variables (or its cousin, a globally visible static class with internal state), however, become usually a problem under the aspect of testability, maintenance and evolvement. When you design a module or component with a clear interface - for example, where it gets something like a "database connection object" passed into, you make this dependency explicitly visible for anyone who has to deal with the component. Moreover, one can mock it out for the purpose of automatic testing.

    When you use a global variable instead, the dependency becomes hidden. That may not be a huge problem for smaller applications, but when an application system contains several hundred thousands lines of code, it becomes sooner or later a maintenance nightmare. Whenever you want to change something here (like allowing to be connected to two different databases at the same time), you will have to audit the whole application code and may have to change it in thousands of places - good luck.

5

Sometimes it's about what you make me read.

When I'm debugging something that relies on a mutable global variable (sorry, depends on) you force me to go read anything that writes to it to understand how my code behaves. That's not fun.

Sometimes it's about moving the code.

When I'm moving code somewhere else I don't want it to know about the world around it because I'm changing that. Anything that makes it aware of the outside now has to be edited. Anything edited has to be retested. That's not fun.

Sometimes it's about testing the code.

When I'm testing code I need to control everything that it depends on to demonstrate what it does under different conditions. Stick me with a global and I have to manipulate it. Since other things likely know about it (cause it's, ya know, global) I now have to ensure none of them is going to break in anyway that I care about.

Sometimes it's about magic.

Sometimes I just don't know what's going on. Even after all these years. Seriously. And when I'm in that head space I just don't need this, "oh and one more thing", to think about. Enough! I want to squeeze all the magic out of the code and make it easy to predict. Globals just have too much pixie dust on them.

That's what we mean by "reason about it". I like code that lives a simple life. Full of short stories. That can stick to one page.

4

When you make something global, you force there to only be one of it.

You say that user authentication status should be global. Then what happens later, when your customers want to be able to log in with two user accounts at once, and switch between them? You keep a backup copy of the authentication state and swap it with the real one? That's asking for bugs.

But your customers aren't asking for that yet. You do want to make an app where, for now, the customer can only log in with one account. Obviously somewhere in the app is going to be a variable for "the account". But you want that to be pushed as far out as possible. You want as much code as possible to assume there could be more than one, so that later when there is more than one, you won't need to change it. Instead of writing void RefreshAuthenticationToken() you write void RefreshAuthenticationToken(Account* account). And even the "log out" button has an Account passed to it, telling it which account to log out when it's clicked on. Only the top level, that ties everything together - the Composition Root - should know there's only one account. Then, when you want more than one, you only have to change how the different parts are tied together.


Everyone knows Minecraft. Minecraft used to have this problem - there was the game world, there was just one, and everything referred to it. But then, they wanted to add an alternate game world (called "the nether") that you could travel to, by going through a portal. The solution was to put The Nether in a separate save file. Whenever the player travelled to The Nether through a portal, the game would essentially delete the player character from the game world, save and quit, load The Nether, then create a player character in The Nether with the same information it previously deleted. For obvious reasons, this doesn't work in multiplayer mode (there can be several players and each one in a different world), so multiplayer didn't support The Nether for several months. Eventually, every function that cared about the game world had a World parameter to say which world, and then both worlds could exist at the same time.

If it was written that way to begin with, the only part of the game that knew there was only one world would be the part that loaded a save file. To create The Nether would've been a lot easier, that part could've been made to load two worlds from a save file instead of just one. It would've worked in multiplayer from the beginning, too. They could've just done new World().setName("The Nether").setTerrain(new NetherTerrainGenerator()).setSkyColour(RED) and had it all ready to go.

2
  • 2
    "Then what happens later, when" - IMO this is a dangerous rationale because it can be used to justify virtually anything. At some point you have to make decisions on what the constraints are going to be. Commented Aug 18, 2022 at 20:45
  • @whatsisname Usually, the ability to have more than one of something is quite a sensible consideration. Commented Aug 18, 2022 at 20:52
3

It is not a problem as long as the clients of the global state are not reused outside the context of the application and the nature of the application doesn't change too much.

The problem is that you will have a (probably backwards) dependency that could be avoided by injecting an interface to the state into the client. Say you have a class Program that creates a form, which creates a number of controls that all need that state. Program has this static GlobalState member. Sure, you could reference Program.GlobalState from anywhere, like from all over your form and controls implementations. But this would require your form and controls to know about that particular Program class which would be, quite literally, backwards.

Now, granted, if your object hierarchy is deep, preventing the global state would require you to pass the state interface in a lot of places which does not make your argument lists any prettier. So there usually is a trade off. Can you be sure there will always be one state, now and forever?

If it ever gets a problem it would be easy to fix. So you may want to shut up any purists with "YAGNI"!.

3

There's a lot of airy claims like "they can't be reasoned about".

Virtually any application will in fact have a few globals, they just get dressed up as singletons, as context objects that get passed around willy-nilly, or as back-tracking links in an object hierarchy. Modern languages have lots of facilities to avoid putting variables formally into the global scope, yet nevertheless allowing them to be accessed from anywhere.

And all programs have an irreducible minimum of data that must flow across the widest scope - if no such flow existed, then it would be possible to divide the program into two separate programs without any loss of function (and after which, variables which previously seemed to be local, would now be global to the whole program).

The problem with globals arises when they are used to compensate for (or enable) a lack of program structure, or when the globals themselves become a menagerie of unorganised variables. It's under these conditions that globals become part of a program that is impossible to reason about.

2

First of all, the question is for what things are global. Global variables in an application are global to the application context. What if the same code is used as an instance of a web application running on an application server. Is it then global to the application server? Possibly not. Global is relative when it comes to applications. Usually it is really "process-global".

But OK, say that the variable is needed at various places and is stateful. Let's use the authenticated user as an example. That doesn't mean that the user has to be a global variable. It can be part of an object that can be retrieved, and that can only be changed by an authentication class. Then you could perform an operation ApplicationContext.getAuthenticationManager().getAuthenticatedUser() and put the state in the authentication manager.

The methods may not even need authentication state, the methods just may want to know if the user is authorized to perform an action. In that case the authentication procedure may be decoupled, and the subprocess could perform ApplicationContext.getAuthorizationHandler.getAuthorizationState(). As you can see, only the ApplicationContext is somewhat global, and everything else resides in nicely encapsulated objects. This is mainly useful if you don't want to pass all these objects around as parameters.

So where does that leave us? Well, we still have mutable objects such as AuthorizationHandler. However, for most objects those are read only tot the relevant parts of the application, and only the part that determines the authorization can change the actual state. However, things like the name of the user cannot directly be changed by most of the application; the state of the application is still protected against access.

0

Should every function anywhere in the application be responsible for checking the user's authentication status?

A well-designed application would separate and encapsulate a concern like authorization into a specific module. For example in Asp.net, authentication is handled by a module in the request pipeline. The regular application code never needs to worry about authentication, since it will never be executed in the first place if the user is not authorized (if authorization is required).

Imagine a bug appears, where the user unexpectedly gets deauthorized. If authorization is encapsulated in a module, you know exactly where to look for the bug, and it is relatively easy to write a unit test ensuring it works correctly. But if authorization status is a global variable, the bug could be anywhere in the application, and there is no way to write a unit test to ensure the bug is eliminated in general.

Software architecture is about managing complexity, by separating concerns into modules (classes in OO) and managing dependencies - other classes that a class can affect or is affected by. With dependency injection, the dependencies of a class are clearly indicated by the constructor.

But a global variable is basically a hidden dependency for every class in the application. And a mutable variable is a hidden dependency from every class to every other class in the application since changing the global might affect code anywhere. A global variable is a communication channel from any place to any other place.

With regular managed dependencies, the number of possible interactions between classes will grow linearity with the number of classes. But the number of possible interactions through global mutable variables will grow exponentially with the number of classes.

But you are right that globals are often convenient. And in a small application with few classes it might not be a big deal.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.