Declarative vs Procedural Programming
The distinction between declarative and procedural programming represents one of the most fundamental concepts in software engineering, particularly relevant when working with infrastructure and system configuration.
The Core Distinction
Procedural programming answers the question: "How do I accomplish this?"
You write step-by-step instructions that the computer executes in sequence. Each step mutates state and moves toward the goal.
Declarative programming answers the question: "What do I want?"
You describe the desired end state, and the system figures out how to achieve it.
Examples in Practice
SQL: Declarative
SELECT name, email FROM users WHERE active = true;You don't tell the database how to find the rows. You declare what you want, and the query optimizer determines the most efficient path.
Shell Script: Procedural
if [ ! -d "/opt/myapp" ]; then
mkdir -p /opt/myapp
fi
if [ ! -f "/opt/myapp/config.json" ]; then
cp /tmp/config.json /opt/myapp/
fi
systemctl restart myappEach step checks current state and takes action. Run it twice, and you might get different results depending on timing.
Nix: Declarative
environment.systemPackages = [ pkgs.git pkgs.vim pkgs.htop ];
services.postgresql.enable = true;You declare what packages should exist and what services should run. Nix computes the difference and applies changes atomically.
Why Declarative for Infrastructure
Idempotency
Declarative systems are naturally idempotent. Apply the same configuration multiple times, get the same result. No "did I already run that script?" anxiety.
Reproducibility
The configuration is the documentation. Give someone your Nix flake, they get your exact system. No "works on my machine" problems.
Version Control Friendly
Declarative configs are data, not scripts. Diffs show what changed in the desired state, not which commands were added.
Easier Rollbacks
Since you're describing complete states, rolling back means applying a previous state description. The system handles the transition.
Trade-offs
Declarative systems often have a steeper learning curve. You're not just learning a new syntax; you're adopting a different mental model.
Procedural approaches can be more flexible for edge cases. When you need precise control over execution order or conditional logic based on runtime state, imperative code is more natural.
The best infrastructure tools provide declarative interfaces with procedural escape hatches when necessary.
How Nix Embodies These Principles
NixOS takes declarative programming to the system level:
- Your entire OS configuration is a single expression
- Changes are computed as derivations (build recipes)
- Applying a configuration doesn't mutate state; it builds a new system generation
- Previous generations remain available for rollback
This isn't just convenience—it's a fundamental shift in how we think about system state.