Pakkit.net
← Back to blog

Engineering Practice

A Config Language Is Still Code

The domain-specific languages we use for policy, config, and rules usually have no static checker and fail late and softly at runtime — which is exactly why they need more engineering discipline than "real" code, not less.

  • Engineering Practice
  • Languages
  • Reliability
  • Code Review
Illustration of a policy configuration document flowing through engineering gates into a runtime engine, showing that config languages need code-review discipline.

A lot of important behavior in big systems lives in a domain-specific language — a policy language, a rules engine, a templating or config dialect that some platform interprets at runtime. People treat these as “just config,” softer and safer than real code. That instinct is backwards. A DSL that decides whether to accept or reject a request is doing the most load-bearing work in the system, and it usually does it with fewer safety nets than a normal programming language — no compiler, no type checker, no test framework worth the name. That gap is exactly why it deserves more discipline, not less.

No static checker means the runtime is your only checker

The defining trait of the policy language I spent a stretch of time in: it had no static checker, and the engine failed late and softly. A typo, a wrong return form, a malformed expression — none of it complained at author time. It surfaced only when a live request happened to hit that exact code path on a running server. Which means a “small config change” could carry a latent crash that wouldn’t appear until production traffic walked into it days later.

Compare that to a typed, compiled language where a whole class of mistakes is caught before the program even runs. Strip that away and every error becomes a runtime error, every runtime error becomes a production incident, and the feedback loop between “I made a mistake” and “I found out” stretches from seconds to days. The language gives you no help, so all the help has to come from you.

A language with no compiler doesn’t have fewer bugs. It just defers all of them to production and removes the tooling that would have caught them.

The traps are the small, silent ones

The bugs that actually bit weren’t dramatic. They were quiet semantic mismatches the engine happily accepted and ran wrong:

  • An overloaded keyword that means different things in different contexts. The same word used to return a value from a function meant something completely different used to signal a disposition at the top level. Mixing the modes was the single most common bug — and nothing flagged it, because both forms were syntactically legal.
  • Operators that look familiar but aren’t. Using a “plus” to join strings when the language reserved a different operator for concatenation. A single bar for a fallback versus a double bar for boolean-or. Familiar-looking syntax that did something subtly other than what a newcomer assumed.
  • Edge cases that demand explicit handling. A null value being a distinct case you had to branch on, separate from the default. Wildcards that matched one level deep, not recursively. Things that seem handled until the one input shape that isn’t.

Every one of these passed straight through to runtime. The language wouldn’t save you; only convention and review would.

So you impose the rigor the language won’t

The team’s answer — and the right one — was to hold the DSL to stricter standards than the language required, precisely because the language enforced nothing. We wrote conventions and treated them as load-bearing:

  • Consistent naming and structure so the shape of a file told you what it was before you read it — files, functions, labels, and variables each with their own casing rule, and a header banner on every file stating its purpose and public surface.
  • Explicit public/private markers on every function group, so the API surface was obvious without chasing callers across files.
  • A registry for anything that emits. Every log code the policy produced had to have a matching row in a codes document, with a script that diffed the code against the registry and failed on drift. If the language won’t track your side effects, you build the thing that does.
  • A pre-merge checklist encoding the traps above: does the return form match the calling context, does every external call handle success/failure/error, does every emitted code exist in the registry. The checklist is the type checker the language never shipped.

This feels like a lot of process for “config.” It’s the right amount of process for code with no compiler that decides production behavior, which is what it actually is.

Treat the legacy dialect as read-mostly

One more thing that saved us pain: the older body of policy predated these conventions, and the temptation was to “clean it up” to match. We didn’t. Reformatting code in a language with no tests and no static checks is how you introduce a silent runtime bug while feeling productive. The rule was: don’t retro-reformat the legacy stuff; mirror the conventions only in new work, and touch the old only when you have a real reason and a way to validate the change. Stylistic churn in untestable code is pure downside.

The lesson generalizes to every “soft” config

You don’t have to work in an exotic policy language to feel this. The same trap lives in giant YAML pipelines, templating dialects, rules engines, infra-as-code, query languages — anywhere a system interprets a text artifact at runtime with weak or no validation. The instinct to treat those as “not really programming” is exactly what gets you a production outage from a one-character change nobody reviewed.

If a thing decides what your system does, it’s code. If it’s code with no compiler and no tests, it needs more of your discipline than the compiled stuff, not a pass because it lives in a .conf file. Give it conventions, give it review, give it a way to fail fast — because the language certainly won’t. The companion move is to build a local harness that catches the late, soft failures before production does. If you wrangle a gnarly config language of your own and have found ways to tame it, I’d love to compare notes.