Runtime Should Depend on The Configuration, Not The Environment

The application runtime should not depend on the environment, but the configuration should. If you've ever written the following code, this post is for you:

function doSomething(){
  if (process.env.NODE_ENV === "development"){
    return { foo: 1 };
  }

  return { foo: 2 };
}

Why Is That A Problem?

Once you start making the application aware of the environment, it automatically adds cognitive load to developers. Next time you want to use that functionality, you will start to think about the environment it runs.

Let's imagine a realistic scenario. Say your application encrypts the HTTP traffic flow in production, but you don't want to enable it during development. It probably looks like this:

function encryptionMiddleware(req, res, next) {
  if(process.env.NODE_ENV === "development"){
    return next();
  }

  const originalFn = res.send;

  res.send = function(){
    arguments[0] = encryptData(arguments[0]);
    originalFn.apply(this, arguments);
  }

  return next();
}

This is a "thing" you have to remember from now on. It is not apparent from the configuration, it is not apparent from the function calls. If you try to follow the code flow, you will see that encryptionMiddleware is registered:

function registerMiddlewares(){
  app.use(encryptionMiddleware);
}

In small codebases, this problem is not immediately noticeable. As you start adding more and more features to the application, there will be more and more cases you want to disable for local development but enable in production. Perhaps you will have even more environments, such as release candidate, beta, alpha, etc.

When that day comes, the application will be very hard to debug. Say you want to enable encryption in the development environment, you change the NODE_ENV to production. Boom, a lot of other features depending on the environment are also activated, it is not possible to activate a single feature without activating others. The only way to do it is to modify the code. This is a bad idea because the tested code and the deployed code will not be the same.

How It Should've Been

You should never have any application code checking for the running environment. Instead, you should have feature flags in the application configuration, for ALL of the functionality that can be switched on/off.

function encryptionMiddleware(req, res, next){
  if (config.flags.encryptResponse){
    const originalFn = res.send;

    res.send = function(){
      arguments[0] = encryptData(arguments[0]);
      originalFn.apply(this, arguments);
    }

    return next();
  }

  return next();
}

This is a good start, but a better code structure can be applied, even in the previous examples.

function registerMiddlewares(){
  if (config.flags.encryptResponse){
    app.use(encryptionMiddleware);
  }
}

This is a lot more visible because you don't need to look into implementation details to see whether this middleware depends on the configuration or not. You can see what is going on with the application on initialization logic.

With this change, the middleware function becomes concise and simple.

function encryptionMiddleware(req, res, next){
  const originalFn = res.send;

  res.send = function(){
    arguments[0] = encryptData(arguments[0]);
    originalFn.apply(this, arguments);
  }

  return next();
}

Conclusion

There are two takeaways from this post.

  1. The application should depend on the configuration variables rather than the environment.
  2. Configuration should be checked on the highest level possible, 99% of the time this belongs to the initialization logic.

If you follow this pattern, the behavior of the application can be understood just by looking at the configuration variables. That is one of the first places others look at when they inspect the application, even your future-self.