Skip to content

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

One file holds the aspects. The fleet.nix tab declares the hosts and users they apply to.

modules/patterns.nix
# 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.

modules/fleet.nix
# 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";
};
};
}

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
ContributeCommunitySponsor