Stop Rewriting Your Nix Config! Use Dendritic Design with Flake Parts
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:
{ 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.
Comments (0)
No comments yet. Be the first to share your thoughts!