Before Making It Configurable |<br>ruguBefore Making It Configurable<br>Written at 2026-05-16Tags:<br>Reflections<br>Configurations exist to allow a program to behave differently without modifying its code. You have a program, you configure it, run it, and it behaves accordingly. In a way, they are like function inputs, but at the application level. They tend to reflect and affect how a system works under the hood. This also makes them closely related to the complexity of our applications.<br>Thinking this way, I cannot think of configurations as just simple inputs. That is why I wanted to think about this topic a bit more and write down some thoughts.<br>Two Kinds of Configurations<br>I think we can group configurations into two types: information-passing and behavior-changing . This distinction is useful because it helps us consider which configurations deserve more attention.<br>Information-passing Configurations<br>In my experience, information-passing configurations are not a big deal. This is because they mostly just pass values around. Whether you pass 5 or 10 of goroutines, or increase or decrease endpoint rate limits, it does not really change how the code is written.<br>To make this more concrete, consider the following example:<br>type Config struct {<br>Workers int<br>RateLimit int
func Process(cfg Config, jobs []Job) {<br>pool := NewWorkerPool(cfg.Workers)<br>client := NewAPIClient(cfg.RateLimit)
pool.Run(jobs)<br>client.Send()
Here, introducing this Config type does not really change how Process is written. It does not matter whether the values come from flags, environment variables, or a configuration file. If we replaced them with default values directly in the code, the overall structure would stay mostly the same.<br>Again, from a code perspective, these kinds of configurations are usually fine. That being said, simply having these values can signal complexity that already exists in the application. For example, we now know there are workers running under the hood and rate limits that need configuration.<br>Whether that is a good thing or not depends on the situation. Sometimes you may actually want to expose these details instead of hiding them, simply to make what is going on more visible. Compared to behavior-changing configurations, I don’t worry much about them.<br>Behavior-changing Configurations<br>Behavior-changing configurations change how the application behaves. They control things like which algorithm to use, whether features are enabled, and so on. I think these are the kinds of configurations we should be more careful about before adding them.<br>Unlike information-passing configurations, they signal the existence of different features being controlled. So, they hint the complexity of the application way better than information-passing configurations.<br>You implement a flag for every possible behavior, you may end up with code looking like this (don’t worry, you are not supposed to read all of it) :<br>type Config struct {<br>UseConcurrentMode bool<br>UseFastAlgorithm bool<br>EnableCache bool<br>UseNewParser bool
func Process(cfg Config, input []byte) Result {<br>if cfg.UseNewParser {<br>input = parseV2(input)
if cfg.EnableCache {<br>if result, ok := cache.Get(input); ok {<br>return result
if cfg.UseConcurrentMode {<br>if cfg.UseFastAlgorithm {<br>return processV2ConcurrentFastWithCache(input)
return processV2ConcurrentSafeWithCache(input)
if cfg.UseFastAlgorithm {<br>return processV2SequentialFastWithCache(input)
return processV2SequentialSafeWithCache(input)
if cfg.UseConcurrentMode {<br>if cfg.UseFastAlgorithm {<br>return processV2ConcurrentFast(input)
return processV2ConcurrentSafe(input)
if cfg.UseFastAlgorithm {<br>return processV2SequentialFast(input)
return processV2SequentialSafe(input)
input = parseV1(input)
if cfg.EnableCache {<br>if result, ok := cache.Get(input); ok {<br>return result
if cfg.UseConcurrentMode {<br>if cfg.UseFastAlgorithm {<br>return processV1ConcurrentFastWithCache(input)
return processV1ConcurrentSafeWithCache(input)
if cfg.UseFastAlgorithm {<br>return processV1SequentialFastWithCache(input)
return processV1SequentialSafeWithCache(input)
if cfg.UseConcurrentMode {<br>if cfg.UseFastAlgorithm {<br>return processV1ConcurrentFast(input)
return processV1ConcurrentSafe(input)
if cfg.UseFastAlgorithm {<br>return processV1SequentialFast(input)
return processV1SequentialSafe(input)
Actually, the nested if-else conditions that you see are a good example of combinatorial explosion . Each new configuration option multiplies the number of states your application can be in.<br>Of course, the previous code was a bit of an exaggeration. We could have rewritten the same thing like this:<br>func Process(cfg Config, input []byte) Result {<br>if cfg.UseNewParser {<br>input = parseV2(input)<br>} else {<br>input = parseV1(input)
if cfg.EnableCache {<br>if result, ok := cache.Get(input); ok {<br>return result
if cfg.UseConcurrentMode {<br>return processConcurrently(input, cfg.UseFastAlgorithm)
if cfg.UseFastAlgorithm {<br>return processFast(input)
return processSafely(input)
Here, we pass...