In Node.js, it’s common to use “module.exports” to export an object instance, allowing other files to get the same object instance when requiring the file in question. A lot of people call this the singleton pattern of Node.js, but this isn’t really a singleton.
No, this is just a cached object instance – and one that is not guaranteed to be re-used across all files that require it.
The Problem With Require
When you call the “require” function in Node, it uses the path of the required file as a cache key. If you require the same file from multiple other files, you typically get the same cached copy of the module sent back to you.
This is great for conserving memory and even producing a poor facsimile of a singleton. However, it’s very easy to break the cached object feature of the require call.
There are two core scenarios in which this feature will not work as expected:
- Accidental upper / lower case letter changes
- When another module installs the same module from NPM
Windows and macOS (by default) are not case sensitive on the file system. You can look for a file called “foo.js” and a file called “FOO.js”, and both of these searches will find the same file in the same folder, no matter the casing on the actual file name. But, we can’t have two different files on OSX or Windows: foo.js and FOO.js. Windows and macOS won’t let us.
With Node.js, though, it doesn’t know that “foo.js” and “FOO.js” are the same file, because Node *is* case-sensitive and it will treat the file names as two separate modules.
Because of this, it’s easy to break the object cache of the require call on both Windows and OSX.
Create a “foo.js” file with a simple export:
Now require it twice, in another file:
Run the index.js file and see what happens:
In this example, the require call used the case sensitive string that you supplied as part of the key for the cache. But, when it came to the file system, the same file was returned both times.
Since it the file was loaded twice, it was also evaluated twice, and it produced the exported object twice – once for each casing of the file name.
There are other problems associated with mis-casing the file name in a require statement, as well. If you deploy to a file system that is case sensitive, for example, the version that is not cased the same as the actual file will not find the file.
While it is generally a good idea to always get the casing correct, it’s not always something that happens. The result is a broken module cache on case insensitive file systems.
NPM Module Dependencies
The other situation where a module cache will not work is when you install the same module as a dependency of two or more modules, from NPM.
That is, if my project depends on “Foo” and “Bar” from NPM, and both Foo and Bar depend on “Quux”, NPM (version 2 or below) will install different copies of “Quux” for each module that depends on it.
I understand that NPM v3 attempts to solve this problem by flattening the dependency list. If Foo and Bar both depend on the same (or compatible) versions of Quux, then only one copy of Quux will be installed.
However, if Foo and Bar depend on different / incompatible versions of Quux, it will still install both versions. Foo and Bar will no share the module cache in this instance.
Make It Cache-Buster-Proof
If the require call in Node doesn’t produce a true singleton, but a cached module instead, how can a true singleton be created?
Unfortunately, the answer is something terrible… a practice that should generally be avoided… global variables.
Node does allow you to export a global variable, in spite of everything it does to try and prevent you from doing this. To do it, you have to explicitly use the “global” keyword:
Now, from anywhere in any other file that is loaded into a node app, with this module module loaded, I can call on “foo”:
The result is an output like the original, but done without holding a direct reference to the required “global” module:
In spite of the seriously dangerous implications of using a global variable, including JSHint complaining about the lack of definition for “foo” in this case, it is possible to create a true singleton in Node with this technique … and to do it somewhat safely, using ES6 symbols.
The Core Of A True Singleton
But with the advent of ES6 (ES2015) Symbols, it is possible to use a global variable and not have it completely destroy the integrity of your application.
Using a Symbol, you can attach something to the global object with relative safety. But having this in place is only half the singleton solution, as you will see in a moment.
Before moving on to the final half of the solution, though, the singleton should provide a specific API to match the pattern definition: an “instance” property by which you can obtain the one single instance of the object.
With this code in place, you can require the same file multiple times, and you will only get one object instance in return. But it does not yet account for the case insensitive file loading, or NPM module dependency problems that we saw earlier.
This is the same output that was shown with the previous inconsistent file name casing. Let’s fix that, and the NPM module loading now.
Creating The True Singleton
The final piece of the puzzle, and the way to create the true singleton, is to ensure that any version of the file being loaded will not overwrite the global symbol. Unfortunately there’s a problem with the way the current symbol is written. Each time the file is loaded, a new instance of the symbol is created.
To fix that, you have to use the global symbol cache. Additionally, you need to check to see if the global object has a value on that symbol already. Finally, you need to give this symbol a unique name, since you are now potentially exposing the symbol to other developers.
This version of the code uses Symbol.for to get a globally shared symbol. It then loads all symbols from the global object, and checks for the presence of the global Symbol you created. If the global symbol already exists, don’t overwrite it.
With this in place, calling the original code with mixed file name casing will produce the same object instance – a true singleton!
A True Singleton… But At What Cost?
With ES6 Symbols, you now have a true singleton that is relatively safe from harm. You can require the same file from multiple places, accidentally mixing the file name casing, and even require it from multiple instances of NPM dependencies. The result will always be a single object instance.
But this solution, as well as it might work, does bring some potential danger of it’s own.
For example, go back to the NPM problem where Foo and Bar depend on Quux. If Foo and Bar depend on different, incompatible versions of Quux, you’re going to be in trouble. The first module to load Quux will be the winner in terms of creating the singleton. The other one will be out of luck and probably end up having strange problems that are very hard to debug.
This can be fixed by inserting a version number into the symbol, for your singleton… but now you’re back at the point where it is no longer a true singleton!
Additionally, this singleton instance is not truly safe. With the use of a global symbol, there is a possibility of someone else trying to use the same symbol for another purpose. This is highly unlikely, but still a possibility – and anytime a problem is “highly unlikely, but still a possibility”, you can rest assured that it will be an actual problem for someone, somewhere.
The Cost-Benefit Analysis Of Both Methods
While it’s true that a simple require call in Node.js will provide a cached export, provided the require statements and versioning are compatible, this object is a poor facsimile of a singleton.
The other side of the coin – creating a true singleton with ES6 Symbols and a global variable – is not without it’s own share of problems.
From experience in using require statements as a poor singleton implementation (… a lot of experience, mind you), I can say that it is generally good enough. Be sure your require statements are cased correctly, and you will likely cover 99% of your needs, or more.
If you do find yourself needing a true singleton to go across NPM dependencies and other module require calls, though, it can be done. Just be sure you actually need this before you head down this path. While it is possible, it is not without it’s own perils.