I think the critical piece you're missing is that the result of require(foo"foo") is always the same object (for some given module name of foo). Consider this REPL example:
So, in your example, there's only one Alpha module. When Beta and Gamma call require("alpha"), they're each getting the same reference to the one-and-only Alpha module singleton object.
Behind the scenes: what's really going on with require caching?
The first time you use require to include a module, Node actually runs the code in that module. The resulting export is stored in require.cache. Subsequent attempts to load the module through require.cache.
require.cache is an object whose keys are file paths with associated values that are objects with module data. For example, assume some module named "foo" in C:\node_modules\foo.js:
> require.cache // empty cache
{ }
> require("foo") // require foo
{ bar: 'baz' }
> require.cache // cache now populated
{ 'C:\\node_modules\\foo.js':
{ id: 'C: \\node_modules\\foo.js',
exports: { bar: 'baz' },
parent: { ... },
filename: 'C:\\node_modules\\foo.js',
...
paths:
[ ... ] } }
The current value of the foo module is in require.cache["C:\node_modules\foo.js"].exports. We can use require.resolve to get the file path of the module from the name, so we can express it also as require.cache[require.resolve("foo")].exports.
When we call require("foo") a second time, Node sees that require.cache[require.resolve("foo")] is defined, and so it returns the value of require.cache[require.resolve("foo")].exports instead of re-running the module creation code. This exports property nested in require.cache is the single instance of the foo module used every time require("foo") is called.
One interesting implication here is that you can delete require.cache[require.resolve("foo")] to force a reload of the foo module with the next call to require("foo"), because the object describing the module is removed from the require.cache object.
So what does this mean for me?
Without require, you could still share values between modules using global variables. For example, your Alpha/Beta/Gamma case would work just as well with just Beta and Gamma setting and reading the global value global.a. Instead, with Node's require system, you're actually setting and reading global.require.cache[require.resolve("alpha")].exports.a, which Node lets you read neatly as just require("alpha").a.
In fact, if you just want to share a value and don't need to import your shared data as a module, you could also use a namespacing object, i.e., have Beta and Gamma set and read properties of the object global.alpha. The major advantage to using require is that you don't need to set up require("alpha") external to the other scripts, whereas you would need to define global.alpha = {} before requireing Beta and Gamma. (Alternatively, you could conditionally define global.alpha in each module based on a typeof global.alpha == 'undefined' check instead, to see if it's the first module to use it.)