The Declarative Trap

Jan 25, 2022

Why do so many declarative systems cause more pain than relief? 3 reasons why I think many declarative systems make the wrong tradeoffs for the majority of users.

Reproducibility over velocity. Bazel is the open-source version of Google's internal declarative build system. It requires you to declare all of your build dependencies before the build begins, with no dynamic dependencies. This ensures that for a given set of inputs, you get the same outputs every time. In theory, it is "reproducible", but in practice, is its nearly impossible to get byte-for-byte reproducibility – it is a spectrum. Inside Google, all external sources are copied in, in the real world, we deal with thousands of nested and dynamic dependencies and external code over the network. Declaring each of them is a painful task. (what about security?)

Verbosity over convention. Determining state can often be more complex than the imperative commands to generate the same state. Verbosity runs wild in declarative configuration like Kubernetes. Engineers end up writing thousands of lines of configuration, or writing code to generate configuration. Convention is difficult to embed into declarative system – the more a system assumes, the more it either becomes rigid or imperative.

Correctness over intention. NixOS uses a declarative package manager that stores packages in a content-addressable way. In many ways this solves some of the issues of dependency hell. Nix packages are written to be "correct", but often don't follow what the intention of the author or user is. Installing a package or setting up an environment can be difficult and complex, even if it is reproducible.

What's the fix? Declarative systems tend to be built in a world that's binary: correct or incorrect, reproducible or not, verbose or bespoke. In reality, each of these trade-offs lies on a spectrum. The answer is somewhere in between.