Config as code: where feature flags fit in
Config as code means your app’s configuration, feature flags included, lives in files in your repository and ships through the same pull request and deploy pipeline as everything else, instead of living in a database behind an admin UI.
Most teams already do this for infrastructure: a Terraform file, a Kubernetes ConfigMap, and a .env checked into the repo. Feature flags get lumped into the same bucket, but a flag carries more than a static value. new-checkout: true does not say who sees it, at what percentage, or why. Bolt a flag onto a generic config file and you get a working toggle with none of that.
This post covers what config as code actually includes, where a feature flag’s extra state breaks a plain config file, and when a dedicated flag tool earns its place over config.yaml and an if statement.
Key Takeaways
- Config as code means configuration ships through the same PR review and deploy pipeline as application code, whether it is a Terraform file, a Kubernetes
ConfigMap, or a flag.- A static config value (
LOG_LEVEL: info) and a feature flag look similar but differ: a flag needs an owner, a targeting rule, and a percentage split, not just a value.- A flat YAML or JSON file is config as code for feature flags, but it stops scaling once you need percentage rollouts, targeting, or a check for call sites left behind by a removed flag.
- Infrastructure tools like Terraform and Kubernetes handle the infrastructure case; a tool like dif handles the flag-specific case, with targeting, bucketing, and validation in CI.
- The underlying workflow is identical either way. A config change is a commit, reviewed in a pull request, with the history in
git log.
What config as code means for a feature flag
Config as code is the practice of keeping configuration, anything that changes your app’s behavior without changing its logic, as files in version control rather than values in a database or an admin dashboard. The twelve-factor app made the general case for it years ago: store config in the environment or alongside the code, not scattered across consoles no one audits.
A Kubernetes ConfigMap is config as code for a running cluster:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info"
MAX_UPLOAD_MB: "25"
Change LOG_LEVEL to debug, open a pull request, merge it, and the new value ships. That is the whole model: a value, reviewed, versioned. A feature flag can start exactly the same way, as a key in a file:
new-dashboard: true
Both lines are config as code. Neither one is enough for a flag that needs to answer “who sees this, and at what percentage.”
Where feature flags outgrow a static config value
A config value like MAX_UPLOAD_MB is a scalar. You set it, and every request reads the same number. A feature flag is closer to a small program: it takes a user, applies a targeting rule, and returns a variant, the same move Martin Fowler’s feature toggles write-up describes. new-dashboard: true cannot express “roll out to 20% of pro-plan accounts and watch dashboard engagement.”
That extra state is what a flag-specific file format adds: an owner, an audience, and a weighted split, instead of one flat key.
---
id: beta-dashboard
status: active
owner: priya@acme.com
surface: dashboard
hypothesis: >
The new dashboard lifts weekly engagement for pro and enterprise accounts.
audience:
include:
- plan: [pro, enterprise]
variants:
- id: "off"
weight: 80
- id: "on"
weight: 20
metrics:
primary: dashboard_engagement
created: 2026-10-03
---
## Brief
Ramp to 50% once weekly engagement holds steady for both variants.
This is still config as code. It is a file, checked into git, reviewed in a pull request. It is just a file format built for the shape a flag actually has, instead of a shape borrowed from infrastructure config.
The git workflow that config as code reuses
The reason config as code works at all, for infrastructure or for flags, is that it reuses a workflow teams already trust instead of inventing a second one.
- Pull request review is the approval flow. Changing
LOG_LEVELor ramping a flag to 50% is a diff someone approves, with the reasoning in the PR description. - git history is the audit log.
git blameanswers who changed a value and when, instead of a dashboard’s activity feed you have to trust separately. - Rollback is a commit. Reverting a bad config change, or a bad flag ramp, is
git revert. Why it happened is in the history, not in someone’s memory. - CI is the validation. A pipeline step can check that a
ConfigMapis well-formed, or that a flag’s weights sum to 100, before either one merges.
The feature flags in git post goes deeper on this workflow specifically for flags. The point here is narrower: none of it is flag-specific. It is config as code, applied to a case that happens to need more structure than a scalar.
Show the file: a flag as config as code
dif is a config-as-code tool built for the flag-specific case. Every flag and experiment is the kind of Markdown file shown above, and dif validate runs in CI the same way a linter checks a ConfigMap:
$ dif validate
✓ all checks passed
$ dif build
✓ validated 1 active experiment(s)
✓ typed client → dif/generated/client.ts
✓ audiences → dif/generated/audiences.ts
✓ events → dif/generated/events.ts
✓ context → dif/context.json
A broken flag fails the pull request like a broken build, not in production. dif build compiles the active files into a typed client, so the call site is one function:
const dashboard = dif("beta-dashboard", {
off: () => <Dashboard />,
on: () => <NewDashboard />,
});
return dashboard();
dif’s own setup is config as code too: dif/config.yaml declares your default surface and audience attributes, the same way a Terraform variables file declares a cluster’s shape. See the .md format and the config reference for the full schema.
The tradeoff: a commit instead of a toggle
Config as code costs you the instant change. Flipping a value in an admin UI takes effect in under a second. Changing a config file, or a flag, is a commit and a deploy, so if your build takes ten minutes, your worst case to change anything is ten minutes.
For most configuration, including most flags, that is a fine trade for the audit trail and the review step. For an ops kill switch on a payment path that needs a sub-second response, a dashboard is the right tool for that one flag, and config as code is not.
When plain config as code is enough
Not every flag needs a dedicated tool. If the value is really a static setting, on for everyone, changed a few times a year, a boolean in a ConfigMap or a .env file is config as code and it is enough. You do not need targeting, bucketing, or a validator for that.
Reach for a flag-specific tool once you need a percentage rollout, an audience rule, or deterministic per-user assignment that a flat key cannot express, or once nobody can tell which of thirty boolean flags in one file are still safe to delete. That second problem is what an owner and a created date on every flag file are for, and once a flag is concluded, dif validate in dif’s CLI warns on any call site still pointing at it.
FAQ
What is config as code? Config as code means configuration, infrastructure settings, feature flags, anything that changes app behavior without changing logic, lives in files in version control. It is reviewed in a pull request and deployed the same way application code is.
Is a feature flag an example of config as code? Yes, when the flag’s definition is a file in your repo rather than a row in a dashboard. A flat new-checkout: true in a JSON file is config as code. So is a dedicated flag file with targeting and a weighted split; it just carries more structure.
What is the difference between config as code and infrastructure as code? Infrastructure as code (Terraform, Kubernetes manifests) provisions and configures the systems your app runs on. Config as code is the broader term: it also covers application-level settings and feature flags, which do not provision anything, they change behavior.
Can a feature flag just be a value in a YAML file? For a simple on/off switch with no targeting, yes. It stops working once you need a percentage rollout, an audience rule, or a way to tell which old flags are safe to remove. That is when a flag-specific file format earns its place.
Do I need a dedicated feature flag tool, or is a config file enough? A config file is enough for static settings. Reach for a dedicated tool once a flag needs an owner, a targeting rule, a weighted split, or validation that catches a broken reference before it merges.
Conclusion
Config as code is the umbrella: configuration, including feature flags, lives in files, gets reviewed in a pull request, and ships through the same pipeline as everything else. A static setting fits in a ConfigMap or a .env file with no extra tooling. A flag that needs an owner, a targeting rule, and a percentage split outgrows a flat key and needs a format built for that shape.
Either way, the workflow is the one you already trust: git history as the audit log, pull request review as the approval, CI as the check. Start with npm install -g @dif.sh/cli, then dif init, to see the flag-specific case for yourself. There is no account to create.