Когда вы вызываете require('module1') нода вычисляет полный путь к модулю и проверяет есть ли в кэше соответствующая запись? Если есть, то возвращается объект exports из этого кэша. Если нет, то создаётся новая запись в кэше и вызывается IIFE function(module) { /* тут ваш файл */ }(cache['/path/to/module1']). Заметим, что поле exports по умолчанию пустой объект. Если вы в своём модуле сразу вызвали require('module2') который в свою очередь сразу вызвал module1, то модулю2 вернётся объект exports из кэша, т.е. пустой. Если же сначала заменить module.exports на то, что нам надо, то он сразу поменяется и в кэше (помним, что объекты передаются по ссылке) и модуль 2 получит наш класс, а не пустой объект.
Ещё раз повторюсь, это очень упрощённое описание и относится только в CommonJS модулям. В ESM (которые import .. from) всё несколько по другому.
// сначала объявляем и экспортируем класс
class Module3 {}
module.exports = Module3;
// а потом уже включаем другой модуль который циклически ссылается на нас.
const module2 = require('./module2');