Devops Developer Tools 1 min read

Stop Rewriting Your Nix Config! Use Dendritic Design with Flake Parts

B
Bright Coding
Author
Share:
Stop Rewriting Your Nix Config! Use Dendritic Design with Flake Parts
Advertisement

Stop Rewriting Your Nix Config! Use Dendritic Design with Flake Parts

Your configuration.nix is a ticking time bomb.

You started simple—one file, one machine, blissful ignorance. Then came the second host. Then Home Manager. Then macOS. Suddenly you're drowning in a sea of import statements, circular dependencies, and copy-pasted boilerplate that breaks every time you sneeze. You've rewritten your structure three times this year. Each "refactor" made things worse.

Sound familiar? You're not alone. The Nix ecosystem gives you unlimited power with zero guardrails. That freedom becomes a prison when complexity explodes.

But what if there was a battle-tested blueprint—a way to structure your Nix code that scales from one laptop to fifty servers without the usual chaos? What if you could stop guessing and start designing?

Enter Dendritic Design with Flake Parts—the architectural pattern that's quietly becoming the gold standard for serious Nix users. This isn't another framework locking you into someone else's decisions. It's a mindset shift that transforms how you build and organize your own Nix code.

Ready to escape the rewrite cycle? Let's dive in.


What Is Dendritic Design with Flake Parts?

Dendritic Design is an architectural pattern for structuring Nix code, originally conceived by mightyiam and now brought to life through the Flake Parts framework in this comprehensive guide by Doc-Steve.

The name "dendritic" evokes tree-like branching structures—just like neurons in your brain, or rivers flowing to the sea. In Nix terms, this means hierarchical, self-similar organization where each "branch" of your configuration is a self-contained, composable unit.

Flake Parts is the secret sauce that makes this pattern practical. It's a modular framework for Nix flakes that eliminates the notorious flake.nix bloat. Instead of cramming everything into a monolithic file, Flake Parts lets you decompose your flake into discrete, reusable modules—each contributing specific outputs, packages, or configurations.

Together, they solve the fundamental paradox of Nix: how to preserve flexibility while imposing enough structure to maintain sanity at scale.

This guide isn't a rigid template you must follow blindly. It's a design vocabulary—a set of patterns, principles, and concrete techniques you adapt to your specific needs. Whether you're managing a single NixOS machine, a fleet of servers, or cross-platform deployments spanning Linux and macOS, Dendritic Design provides the scaffolding.

The repository has gained serious traction among developers who've "been there, done that" with ad-hoc Nix structures. It's particularly valuable if you're contemplating your third refactor—or if you're smart enough to prevent the first two disasters.


Key Features That Make This Pattern Irresistible

Composable "Feature" Architecture

At the heart of Dendritic Design lies the feature—a self-contained unit of functionality. Unlike traditional Nix modules that often sprawl across files and concerns, a feature bundles related configuration, packages, and logic into a cohesive whole. Features compose cleanly: import what you need, ignore what you don't.

Aspect-Oriented Design Patterns

The guide introduces eight distinct aspect patterns that solve recurring structural challenges:

  • Simple Aspect: Direct, one-to-one configuration mapping
  • Multi Context Aspect: Same feature, different behaviors per host/user/environment
  • Inheritance Aspect: Hierarchical configuration with clean overrides
  • Conditional Aspect: Toggle features based on predicates
  • Collector Aspect: Aggregate configurations from multiple sources
  • Constants Aspect: Centralized, type-safe configuration values
  • DRY Aspect: Eliminate repetition without obfuscation
  • Factory Aspect: Generate configurations programmatically

These aren't academic exercises—they're survival tools for real-world complexity.

Flake Parts Integration

By leveraging Flake Parts, you get:

  • Empty flake.nix: Your entry point becomes a clean manifest, not a thousand-line monster
  • Modular outputs: Packages, devShells, NixOS configurations, and home configurations live in separate files
  • Automatic merging: Flake Parts handles the tedious wiring you'd otherwise do manually
  • Extensibility: Third-party Flake Parts modules plug in seamlessly

Cross-Platform Native

The pattern embraces NixOS, nix-darwin, and Home Manager as first-class citizens. No more maintaining parallel structures or awkward platform switches. One mental model, all platforms.

Discoverability and Debugging

Because structure follows predictable patterns, you know where to look when things break. That 3 AM production incident? Tracing from symptom to root cause becomes minutes, not hours.


Real-World Use Cases Where Dendritic Design Shines

Use Case 1: The Growing Homelab

You started with one Raspberry Pi running NixOS. Now you have six hosts: a NAS, a media server, a reverse proxy, two development machines, and a backup server. Each shares some configuration (SSH hardening, monitoring agents) but needs unique services. Dendritic Design's Multi Context Aspect lets you define "base security" once, then specialize per host without copy-paste nightmares.

Use Case 2: Cross-Platform Developer Environment

Your team uses Nix for reproducible environments—but half the team runs NixOS laptops, half runs macBooks with nix-darwin, and everyone uses Home Manager for dotfiles. The Inheritance Aspect creates a common "developer" feature that works everywhere, with platform-specific tweaks cleanly separated. No more "works on my machine"—it works on every machine.

Use Case 3: Multi-Tenant Service Deployment

You're deploying customer-facing services where each tenant needs isolated instances of the same application stack, but with different resource limits, domains, and feature flags. The Factory Aspect generates tenant configurations from a schema, while the Constants Aspect ensures pricing tiers and limits are defined in one authoritative location.

Use Case 4: Gradual Migration from Legacy Config

Your existing Nix setup is a configuration.nix that's grown to 2,000 lines. You can't refactor everything at once—the system must stay operational. Dendritic Design's modular nature lets you extract features incrementally. Move SSH config to a feature this week, networking next week. The Collector Aspect gradually replaces your monolith without a big-bang rewrite.


Step-by-Step Installation & Setup Guide

Getting started with Dendritic Design requires understanding the foundation. This isn't a single package to install—it's an architectural approach applied through Flake Parts.

Prerequisites

Ensure you have:

  • Nix with flakes enabled (experimental-features = nix-command flakes)
  • Basic familiarity with Nix expressions and module system
  • Git for cloning reference implementations

Step 1: Initialize Your Flake with Flake Parts

Create a minimal flake.nix that delegates to Flake Parts:

{
  description = "My Dendritic Configuration";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-parts.url = "github:hercules-ci/flake-parts";
    flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
  };

  outputs = inputs@{ flake-parts, ... }:
    flake-parts.lib.mkFlake { inherit inputs; } {
      # Your flake parts modules will be imported here
      imports = [
        # Feature modules go here
      ];
      systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
    };
}

Notice how sparse this is? That's intentional. The complexity lives in well-organized modules, not your entry point.

Step 2: Establish Your Directory Structure

Create the dendritic skeleton:

.
├── flake.nix              # Minimal entry point
├── flake.lock             # Generated
├── features/              # Your feature modules
│   ├── base/              # Shared across all hosts
│   ├── development/       # Dev tools, languages
│   ├── graphical/         # Desktop environment
│   └── hosts/             # Host-specific features
│       ├── laptop-nixos/
│       ├── workstation-mac/
│       └── server-headless/
├── lib/                   # Your custom library functions
├── aspects/               # Aspect pattern implementations
└── hosts/                 # Host configurations (thin wrappers)
    ├── laptop-nixos/
    └── workstation-mac/

Step 3: Create Your First Feature

A feature is a Flake Parts module. Create features/base/default.nix:

{ config, lib, pkgs, ... }:

{
  # This feature provides NixOS configuration
  flake.nixosModules.base = { config, pkgs, ... }: {
    # Your NixOS module content here
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true;
    
    # Essential system packages
    environment.systemPackages = with pkgs; [
      vim
      git
      htop
    ];
    
    # Enable flakes
    nix.settings.experimental-features = [ "nix-command" "flakes" ];
  };
}

Step 4: Wire Features into Hosts

In hosts/laptop-nixos/default.nix:

Advertisement
{ inputs, ... }:

{
  imports = [
    inputs.self.nixosModules.base
    inputs.self.nixosModules.graphical
    inputs.self.nixosModules.hosts.laptop-nixos
  ];

  # Host-specific settings
  networking.hostName = "laptop-nixos";
  system.stateVersion = "24.05";
}

Step 5: Build and Deploy

# Build your NixOS configuration
nix build .#nixosConfigurations.laptop-nixos.config.system.build.toplevel

# Or switch directly (if on the target host)
sudo nixos-rebuild switch --flake .#laptop-nixos

Real Code Examples from the Repository

Let's examine concrete patterns from the Dendritic Design guide. These aren't toy examples—they're production-ready techniques.

Example 1: The Simple Aspect

The simplest aspect pattern maps one configuration source to one output directly. This is your starting point before complexity demands more sophisticated patterns.

# features/ssh/default.nix
{ config, lib, pkgs, ... }:

{
  # A Simple Aspect: direct feature-to-module mapping
  flake.nixosModules.ssh = { config, pkgs, ... }: {
    services.openssh = {
      enable = true;
      # Security hardening as default
      settings = {
        PasswordAuthentication = false;
        PermitRootLogin = "no";
        X11Forwarding = false;
      };
    };
    
    # Ensure sshd starts
    systemd.services.sshd.wantedBy = [ "multi-user.target" ];
  };
}

This pattern works beautifully for universal features—configurations that apply identically everywhere. The SSH hardening above doesn't vary by host, so no abstraction overhead is needed.

Example 2: The Multi Context Aspect

Here's where Dendritic Design gets powerful. The same feature behaves differently based on context—host type, user role, or environment.

# features/monitoring/default.nix
{ config, lib, pkgs, ... }:

let
  # Define contexts as a structured attribute set
  contexts = {
    server = {
      agent.enable = true;
      server.enable = true;  # Runs Prometheus/Grafana
      scrapeInterval = "15s";
      retention = "30d";
    };
    workstation = {
      agent.enable = true;
      server.enable = false;  # Only node exporter
      scrapeInterval = "60s";
      retention = null;
    };
    minimal = {
      agent.enable = false;   # Monitoring disabled
      server.enable = false;
      scrapeInterval = null;
      retention = null;
    };
  };
in
{
  # Expose context definitions for host selection
  flake._contexts.monitoring = contexts;

  # The feature module accepts context as parameter
  flake.nixosModules.monitoring = { config, pkgs, ... }: 
    { context ? "workstation" }:
    let
      cfg = contexts.${context};
    in
    {
      # Conditional configuration based on context
      services.prometheus.exporters.node.enable = cfg.agent.enable;
      
      services.grafana = lib.mkIf cfg.server.enable {
        enable = true;
        settings.server.http_port = 3000;
      };
      
      services.prometheus = lib.mkIf cfg.server.enable {
        enable = true;
        globalConfig.scrape_interval = cfg.scrapeInterval;
        retentionTime = cfg.retention;
      };
    };
}

Why this matters: Without this pattern, you'd maintain three nearly-identical monitoring modules or pepper mkIf everywhere. The Multi Context Aspect centralizes variation points, making intent explicit and maintenance trivial.

Example 3: The Collector Aspect

When multiple features need to contribute to a shared configuration—like firewall rules, system packages, or systemd services—the Collector Aspect aggregates contributions automatically.

# aspects/collect-packages.nix
{ config, lib, ... }:

{
  # Define a collector that merges package lists from all features
  perSystem = { config, pkgs, ... }: {
    # This creates a unified package set from feature contributions
    _module.args.collectedPackages = 
      lib.concatLists (lib.attrValues config._featurePackages or {});
  };

  # Features declare their packages in a namespaced attribute
  options._featurePackages = lib.mkOption {
    type = lib.types.attrsOf (lib.types.listOf lib.types.package);
    default = {};
    description = "Package contributions from each feature";
  };
}

# In a feature, contribute packages:
# features/development/default.nix
{ config, lib, pkgs, ... }:

{
  # Declare packages this feature contributes
  _featurePackages.development = with pkgs; [
    nodejs_20
    rustup
    python311
    docker
  ];
}

The collector pattern eliminates the manual environment.systemPackages = [ ] ++ importA ++ importB ++ ... dance. Each feature declares its contributions; the aspect collects them. This is the Dendritic principle of self-similarity: features don't know about each other, yet compose harmoniously.

Example 4: The Factory Aspect

For programmatic configuration generation—think user accounts from a team roster, or microservices from a service catalog:

# features/users/default.nix
{ config, lib, pkgs, ... }:

let
  # Schema for user definitions
  mkUser = { name, email, groups ? [ "users" ], sshKeys ? [] }: {
    inherit name;
    users.users.${name} = {
      isNormalUser = true;
      extraGroups = groups;
      openssh.authorizedKeys.keys = sshKeys;
      # Derive shell preference from group membership
      shell = if lib.elem "wheel" groups then pkgs.zsh else pkgs.bash;
    };
    # Git configuration per user
    home-manager.users.${name}.programs.git = {
      enable = true;
      userName = name;
      userEmail = email;
    };
  };
in
{
  # Factory function exposed for host configurations
  flake.lib.userFactory = mkUser;

  # Or pre-define team members as features
  flake.nixosModules.users.alice = mkUser {
    name = "alice";
    email = "alice@example.com";
    groups = [ "users" "wheel" "docker" ];
    sshKeys = [ "ssh-ed25519 AAAAC3NzaC..." ];
  };
}

The Factory Aspect shines when you have many similar-but-not-identical configurations. Define the schema once, instantiate everywhere.


Advanced Usage & Best Practices

Start Simple, Evolve Deliberately

Don't apply every aspect pattern from day one. Begin with Simple Aspects, introduce Multi Context when you have actual variation, and add Collectors when manual aggregation becomes tedious. Premature abstraction is as harmful in Nix as anywhere else.

Feature Naming Conventions

Use descriptive, domain-driven names: features/networking/wireguard, features/services/postgres, not features/module1. The dendritic structure is self-documenting when names are honest.

Leverage Flake Parts' imports System

Flake Parts modules can import other Flake Parts modules. Create "meta-features" that bundle related functionality:

# features/workstation/default.nix
{ ... }:
{
  imports = [
    ../base
    ../development
    ../graphical
    ../browsers
    ../communication
  ];
}

Version Your Aspects

As your understanding deepens, aspect patterns evolve. Keep a lib/aspects/ directory with versioned implementations (collector-v1.nix, collector-v2.nix) to avoid breaking existing features during transitions.

Test with nix flake check

Flake Parts integrates beautifully with flake checks. Validate your structure:

# In your flake-parts module
perSystem = { pkgs, ... }: {
  checks = {
    # Verify all features evaluate without error
    feature-syntax = pkgs.runCommand "feature-check" {} ''
      # Your validation logic
      touch $out
    '';
  };
};

Comparison with Alternatives

Approach Structure Flexibility Learning Curve Best For
Monolithic configuration.nix Single file None Low Single host, quick start
Manual imports Ad-hoc splitting Medium Low Simple multi-file setups
Flake-utils Template-based Medium Medium Standard flake outputs
Flake Parts alone Modular framework High Medium Complex flakes, team use
Dendritic Design + Flake Parts Pattern-based architecture Very High Medium-High Scaling, maintainability, cross-platform
Deploy-rs/Colmena Deployment-focused Low Medium Fleet deployment only
NixOS generators/images Image builders Low Medium Immutable infrastructure

The critical difference: Other approaches give you tools or templates. Dendritic Design gives you a design language—a way to think about structure that transcends any specific implementation.


Frequently Asked Questions

Is Dendritic Design only for complex setups?

Not exclusively, but it's where the payoff is largest. Single-host users can benefit from the mental model, though the overhead may not justify itself until you have multiple hosts or platforms.

Do I need to learn all eight aspect patterns?

Absolutely not. The guide's FAQ explicitly states this. Start with Simple and Multi Context, add others as pain points emerge. The patterns are a toolbox, not a curriculum.

Is Flake Parts the same as Dendritic Pattern?

No—this is a common confusion. Flake Parts is the modular framework for flakes. Dendritic Pattern is the architectural philosophy for organizing Nix code. They work beautifully together but are independent concepts. You could apply Dendritic principles without Flake Parts (though it's harder), or use Flake Parts without Dendritic structure (missing the organizational benefits).

Why is my flake.nix nearly empty?

That's the Flake Parts magic! All the actual configuration lives in imported modules. The empty flake.nix is a feature, not a bug—it means your entry point is a readable manifest, not an unmaintainable monster.

How does this compare to template repositories like nix-starter-configs?

Templates give you a starting point. Dendritic Design gives you a way of evolving that starting point as requirements grow. Templates are copied; patterns are internalized.

Can I migrate incrementally from my existing config?

Yes! The Collector Aspect is specifically designed for gradual migration. Extract features one at a time, keep legacy imports working, and remove them when ready.

Is this overkill for my use case?

If you're running one NixOS machine with no plans to grow—probably. But if you've already rewritten your structure once, or feel the tension of growing complexity, Dendritic Design prevents the next rewrite.


Conclusion: Design Your Way Out of Nix Chaos

The Nix ecosystem's flexibility is both its greatest strength and its most dangerous trap. Without deliberate structure, that freedom curdles into unmaintainable complexity. You've felt it—the dread of touching configuration.nix, the paralysis before a "simple" refactor, the 2 AM debugging through layers of imports you no longer understand.

Dendritic Design with Flake Parts offers escape velocity. It's not a framework that limits your choices; it's a pattern language that amplifies your good choices and makes them composable, discoverable, and scalable.

The repository's comprehensive example and detailed wiki transform abstract concepts into working code. Whether you're architecting a homelab, unifying team environments, or finally taming that config that's grown too many heads, this guide meets you where you are.

Stop rewriting. Start designing.

Clone the repository, explore the patterns, and join the growing community of Nix users who've discovered that structure isn't the enemy of flexibility—it's the prerequisite.

Star the repo, dive into the FAQ, and share your dendritic configurations. The future of your Nix codebase will thank you.

Advertisement

Comments (0)

No comments yet. Be the first to share your thoughts!

Leave a Comment

Apps & Tools Open Source

Apps & Tools Open Source

Bright Coding Prompt

Bright Coding Prompt

Categories

Advertisement
Advertisement
Advertisement