Write code, not configuration

When you write a program which might use one of multiple implementations, or needs to connect to some URL, or needs some information to function, your first step should be to just hardcode it. Use one implementation, hardcode one URL, use one specific piece of information.

If, after doing so, you find that you need another program with different hardcoded information, turn your program into a function. Take the implementation as an argument, take the URL as an argument, take the information you need as an argument.

And then just call that function with the arguments you prefer, each time you need a program with different "configuration".

You don't need to load those arguments from some file on disk, or by querying some database or service, or even by taking them as command line parameters.

Just make a separate program for each case.

For a batch job, this might mean a few different executables, tweaked and rebuilt frequently over time as one's needs change.

For a user-facing application, this might mean that each user runs their own custom executable, compiled for or by them, with pre-compiled shared libraries common between all users.

For a daemon providing some network service and connecting to other services, this might mean 10 or so different executables, which are run in the 10 or so different availability zones or datacenters or sorts of machines on which this daemon is deployed. Or for a more heterogeneous deployment, it might mean many thousands of different executables, deployed to many thousands of different environments, each layered on top of a common image with shared libraries identical between all deployments.

If you want to share information between multiple programs with different configurations, share it in the same way you share code: with a library.

If you want to make your configuration more dynamic, write code to dynamically determine the arguments to pass.

If you want to make rapid changes and don't want to wait for builds, call the function from an interpreted language, or rely on fast incremental builds.

If you want to see what arguments are being passed to your function in your program, use logging and debuggers, according to your preference.

If you want to roll back to an old configuration, just roll back to an old binary, which you need to be able to do anyway.

Code written in this way in your actual programming language can be easily constrained to only allow valid configurations; that's much more difficult for code written in a configuration DSL, like JSON, YAML, or Dhall.

Writing code in your general-purpose language is easier, faster, and better than writing separate configuration.

Of course, this is all easier with faster and more powerful build systems, like Nix, or with interpreted or fast-building languages, like Python, or with, at least, shared libraries and a fast link step, like dynamic libraries in C.

If you're on a slow, weak, and hard-to-use build system, with a slow-building language, and you have slow linking, then you probably want to fix one or more of those issues first; although if your programs are small, even those issues are not necessarily prohibitive.

And you might also be in a corporate environment, where code changes require an extensive and painful process, but configuration changes can be made relatively easily. If so, perhaps this article will help you improve the situation.

Many have written about this before; here is another article about this.