Originally posted as TotW #45 on June 3, 2013
*by Titus Winters
“What I really want is the behavior of my code to be controlled by a global variable that cannot be statically predicted, whose usage is incompletely logged, and which can only be removed from my code with great difficulty.” – Nobody, Ever
The common use of flags in production code, especially within libraries, is a mistake. Don’t use flags unless it is truly necessary. There, we said it.
Flags are global variables, only worse: you can’t know the value of that variable by reading the code. A flag may not only be set at startup, but could potentially be changed later through arbitrary means. If you run a server in your binary, there is usually no guarantee that the value of your flag will remain unchanged from cycle to cycle, nor any notification if it were to change, nor any means for looking for such a change.
If your production environment logs the invocation of every binary directly, and stores those logs, great. Most environments don’t work that way. For library code, this uncertainty is especially nefarious: how can you tell when use of a particular feature is actually dead? The simple answer is: you can’t.
Flags also make it challenging to put the final stake in dead code. During
migrations to new backends, you’d think that removing legacy code would
simply be a matter of removing unnecessary build dependencies and issuing
history’s most satisfying
git rm. You’d be wrong. If your legacy binary
has hundreds of flags defined and referenced by production code,
simply removing the dead code would cause massive problems for your release
engineers: almost no jobs would start up after such a change.
The worst part of all of this? An analysis that was done at Google during early 2012 found that the majority of C++ flags had never actually varied, as far as we can tell within the data retention limits described above.
Flags are appropriate in certain use cases, however: debugging without
flag-enabled backtraces just wouldn’t be the same. Feature flags, when
handled properly (and cleaned up afterward), are perfectly justified. Knobs
that genuinely need to be tweaked by heroic SREs are a handy safety net.
More broadly, flags used to pass name/value inputs to a binary, and
used only in
main(), are much more maintainable than positional parameters.
Even given those caveats, it’s time that we all take a good hard look at our usage of flags. The next time you’re tempted to add a flag to your library, spend a while looking for a better way. Pass configuration explicitly: this is almost always easier to reason about properly, and is certainly easier to maintain. Consider making numeric flags into compile-time constants. If you encounter new flags in code reviews, push back. Every flag introduction should be justified.