For about three years, I put everything in environment variables. Database URLs, API keys, feature flags, the name of the S3 bucket we used for staging — all of it lived in .env files and shell exports and CI/CD secrets panels. It felt clean. It felt like the right way to do things. Then I inherited a codebase where someone had taken this philosophy to its logical extreme, and I spent two weeks trying to figure out why the application behaved differently in production than in staging, and the answer was a typo in an environment variable name that nobody had caught because there were 47 of them.
I'm not saying environment variables are bad. They're not. They're the right tool for a specific job: injecting runtime configuration that varies by environment, especially secrets that shouldn't live in version control. But somewhere along the way, the industry decided that environment variables were the answer to all configuration problems, and that's where things went sideways.
The problem with environment variables is that they're invisible. They exist outside your codebase. They're not versioned. They're not documented in any meaningful way unless you maintain a separate document that inevitably falls out of sync. When something breaks because of a missing or malformed environment variable, the error message is usually unhelpful — you get a null pointer exception or a connection refused, not "hey, DATABASE_URL is missing the port number."
What I use now is a layered approach. Secrets — actual secrets, things that would be bad if they leaked — still go in environment variables or a secrets manager. But everything else: configuration files. Checked into version control. Validated at startup. With defaults that make sense. With comments explaining why each value exists.
The dev culture around this has been shifting. More teams are using tools like Vault or AWS Secrets Manager for actual secrets, and keeping everything else in config files that are part of the repo. The distinction matters: a secret is something that grants access. A configuration value is something that describes how the system should behave. These are different things and they deserve different treatment.
Validation is the part that changed everything for me. When your application starts up and reads its configuration, it should validate every value it needs. Not just "is this variable set" but "is this value a valid URL" and "does this port number make sense" and "is this feature flag one of the expected values." If validation fails, the application should refuse to start and tell you exactly what's wrong. This sounds obvious. It is obvious. Most applications don't do it.
The AI and dev tooling space has been interesting to watch here. A lot of AI-adjacent applications have extremely complex configuration — model endpoints, API keys, rate limits, fallback behaviors — and the ones that handle it well have invested in proper configuration management. The ones that don't are the ones where you spend an afternoon debugging why the model is returning garbage and eventually discover that the temperature parameter was being read as a string instead of a float.
There's also the question of documentation. A config file in your repo can have comments. It can have a schema. It can have a README that explains what each value does and what the valid options are. An environment variable has none of that unless you build it separately. I've started treating configuration documentation as part of the code, not as an afterthought. If you can't explain what a configuration value does in a comment, you probably don't understand it well enough to be using it.
The fringe tech community has been doing interesting things with configuration as code — treating your entire infrastructure configuration as a typed, validated, version-controlled artifact. That's the direction things are moving. The environment variable as the primary configuration mechanism is a historical artifact of how Unix systems worked, not a principled design choice.
I still use environment variables. I use them for secrets. I use them for the handful of values that genuinely need to vary between environments without a code change. But I've stopped using them as a catch-all. The config file isn't the enemy. It's the thing that makes your system legible to the next person who has to work on it, which is often you, six months from now, at 2am, trying to figure out why something stopped working.
The systems thinking that I find most useful is the kind that asks: what happens when this breaks? With environment variables, when something breaks, you're often debugging in the dark. With a validated config file, you get a clear error at startup. That's not a small difference. That's the difference between a two-minute fix and a two-hour investigation.
Start with the question: is this a secret, or is it configuration? If it's a secret, use a secrets manager. If it's configuration, put it in a file, check it in, validate it, document it. Your future self will thank you. Probably at 2am.