The Nix module system isn’t a NixOS feature. It’s a generic mechanism for typed, composable configuration that NixOS happens to be one consumer of. A module is a function returning two attribute sets: options (typed parameters) and config (values to set), with the body receiving the merged configuration as input. The merge is a fixed point. Every module sees the final composition, including its own contribution. From this primitive: mkDefault/mkForce give layered priorities, mkIf lazily conditionalizes contributions, type errors surface at eval time, and modules import other modules to form a tree.
The consequence is that modules don’t need to know about each other. Module A declares an option, module B sets it, and the merge wires them together. That’s what services.nginx.enable = true; actually does, and it’s what flake-parts applies to flake outputs. A third-party flake can ship a flakeModule (treefmt-nix, git-hooks.nix, easy-hosts, devshell) that adds options to your flake; you import it, set values, and it contributes outputs on your behalf.
Function-based frameworks like flake-utils cannot replicate this. A function takes inputs and returns outputs; it can’t merge with another function call to form a fixed point. That property, not the syntactic boilerplate around forAllSystems, is the structural reason flake-parts won. The pattern’s theoretical ancestry is multi-dimensional separation of concerns (Tarr et al., 1999); its practical surface shows up every time a Nix - Home Manager config grows large enough to need plug-in tooling.