The dendritic aspect patterns
Den is one way to write dendritic Nix. The dendritic design guide names eight aspect patterns that recur in dendritic configurations. Most Den users know them, by these names or by others. This page gives the yen form of each.
In the guide, each pattern is a different way to write flake.modules.<class>.<aspect> and its imports. In yen
each pattern is an aspect. What changes is its at (a predicate), or the data it reads.
| Pattern | Guide | yen |
|---|---|---|
| Simple | a module per class | an aspect with one key per class, below |
| Multi context | a NixOS module adds a home module to home-manager.sharedModules |
an aspect at the users within the hosts that have the feature: System and home |
| Inheritance | imports = [ parent ] |
the parent’s at names the child aspect, below |
| Conditional | lib.mkMerge and lib.mkIf pkgs.stdenv.isLinux |
at tests a fact; not excludes, below |
| Collector | each host feature adds its part to a shared module | a sink at a common parent, read back with elements: Facts from other hosts |
| Constants | a generic class module with options.systemConstants |
a field of the fleet, read as the fleet arg, below |
| DRY | a new class for values inside an attribute set of submodules | a sink that places its content at a path: Custom classes |
| Factory | a function that returns modules, such as user "bob" true |
a kind and its instances, with fields for the arguments, below |
The patterns in yen
Section titled “The patterns in yen”One file holds the aspects. The fleet.nix tab declares the hosts and users they apply to.
# Five of the dendritic aspect patterns, in yen. Each one is an aspect; only its `at` differs.{ yen, ... }:let inherit (yen.lib) all not;in{ # Simple aspect: one feature, for every host. yen.aspects.basic-packages = { # (1) at = yen.kinds.host; nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.git ]; }; };
# Inheritance aspect: system-desktop extends system-cli. system-cli applies wherever system-desktop does. # There is no `imports`, so a parent included by two children is still given once. yen.aspects.system-cli = { # (2) at = [ yen.host.srv yen.aspects.system-desktop yen.aspects.system-dev ]; nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.tmux ]; }; }; yen.aspects.system-desktop = { at = yen.host.desk; nixos.services.printing.enable = true; }; yen.aspects.system-dev = { at = yen.host.desk; # a second child on the same host: system-cli still comes once nixos.virtualisation.docker.enable = true; };
# Conditional aspect: no lib.mkIf inside a module. `at` tests a fact, and can exclude. yen.aspects.nvidia = { at = s: (s.gpu or null) == "nvidia"; # (3) nixos.services.xserver.videoDrivers = [ "nvidia" ]; }; yen.aspects.headless = { at = all [ yen.kinds.host (not yen.aspects.system-desktop) # (4) ]; nixos.documentation.enable = false; };
# Constants aspect: a field of the fleet, read as the `fleet` arg. No options, no generic class. yen.aspects.acme = { at = yen.kinds.host; nixos = { fleet, ... }: { security.acme.defaults.email = fleet.adminEmail; # (5) }; };
# Factory aspect: `user "bob" true` becomes an instance with a field. The kind is the factory. yen.aspects.user-account = { at = yen.kinds.user; nixos = { user, ... }: { users.users.${user.name} = { isNormalUser = true; extraGroups = if user.admin then [ "wheel" ] else [ ]; # (6) }; }; };}(1) is the simple pattern: at is the host kind, so the aspect is active at every host.
(2) is inheritance: system-cli is active at srv, and wherever system-desktop or system-dev is active.
(3) is the conditional pattern: at is a function that tests the gpu field of the scope.
(4) excludes: headless is active at every host where system-desktop is not active.
(5) is the constants pattern: fleet is the arg that holds the fleet, and adminEmail is its field.
(6) is the factory pattern: admin is a field of the user kind, so each user is one call of the factory.
# Two hosts and two users. The fleet carries constants; a user's `admin` field replaces a user factory's argument.{ yen, inputs, ...}:let inherit (yen.lib) opt;in{ yen.kinds.fleet = { fields.adminEmail = yen.lib.str; spawns.host = _: yen.host; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.host = { fields.gpu = opt (yen.lib.enum [ "none" "nvidia" ]) "none"; spawns.user = yen.lib.pointing "hosts" yen.user; sinks.nixos.out = { host, nixos, ... }: { nixosConfigurations.${host.name} = inputs.nixpkgs.lib.nixosSystem { modules = nixos; }; }; }; yen.kinds.user.fields = { hosts = yen.lib.listOf yen.kinds.host; admin = opt yen.lib.bool false; };
yen.fleet.main.adminEmail = "admin@example.org"; yen.flake = { out, ... }: out;
yen.host.desk.gpu = "nvidia"; yen.host.srv = { }; yen.user.bob = { hosts = [ yen.host.desk ]; admin = true; }; yen.user.eve.hosts = [ yen.host.desk yen.host.srv ]; yen.aspects.machine = { at = yen.kinds.host; nixos = { host, ... }: { networking.hostName = host.name; boot.loader.grub.device = "nodev"; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "25.05"; }; };}Pitfalls that go away
Section titled “Pitfalls that go away”The guide warns about these. In yen they cannot happen, or they have no cause:
| Guide warning | In yen |
|---|---|
Use lib.mkMerge, not //, to merge aspect parts |
An aspect written in many files is read from all of them. Nothing is merged with //. |
Multi context plus inheritance can add the same module to sharedModules two times |
An element has one id per receiver. A parent reached by two children is given once. |
Never use lib.mkIf around imports: infinite recursion |
No module looks at its own config to decide if it applies. at decides, before NixOS runs. |
Every host needs a mkNixos call |
The host kind builds every host: Many hosts, one base |