Dependency injection, perhaps? Part 2

August 27, 2021

In the previous post in this series, I tried to outline some of the problems I see when using Component based libs in Clojure. In this post, I’ll look at a couple of problematic areas I discovered in our codebase related to how we do configuration in conjunction with Component. In this post I’ll use external config to mean configuration which is provided from the running environment, and internal config to mean configuration which is like global constants and which are not changed between environments.

It is generally accepted that externalizing configuration is a Good Thing(TM). And I’m not going to argue against that, but I will like to add that the more configurable an app is, the harder it is to work with as a dev, since you don’t necessarily know what value a certain thing will have during runtime. For some values, like passwords, that’s great. For other values, not so great.

At Ardoq, we’ve used a library for handling our “configuration”, clj-configurator. This lets you have a map of default configuration, which you then pass to the library which then, according to rules, goes into the environment to look for values to overrride the default values. This is almost all good. Apart from the fact that now suddenly, your whole config, ie both your internal config (stuff that is basically just global variables) and your external config (stuff that should be configured from the outside) is configurable from the outside, and you need to mentally keep track (or read your infrastructure as code code to figure out what are constants, and are things that are in fact configurable from the outside.

Another question one might ask oneself regarding the internal config, ie values which are constants and are not injected from the runtime environment, is where those values should be stored. It’s tempting to stick this in a config-like map, but IMO this is not really a great solution. Consider the two fns:

(defn do-something [config & args]
  (let [global-value (get-in config [:foo :bar :baz])]
      (whatever global-value args)))
(defn do-something [some-value & args]
   (whatever some-value & args))

The first do-something needs to have a (potentially) nested map passed as the first parameter, whereas the second just receives some value. In the second case, if some-value is a true constant, you could even omit the parameter all together and reference a var directly from inside the fn. Beware, there are trade-offs here.

Should we use Components to hold internal config?

Again, at Ardoq, we’ve used our Components for also holding config, no matter if that config is external or internal. So typically, you’d have something like:

(defrecord MyService [config])

(defn do-something [{:keys [config]} & args]
  (let [some-value  (get-in config some-path)]

and you’d call a fn in this way:

(do-something my-service some other params)

Somewhat nice, but it’s not really great from a maintenance point of view, especially if the value you’re pulling from the config is an internal configuration. The reason for this is that in our case, you really have to do quite a bit of digging to understand what the value of some-value is. Is it internal or is it external, does it vary between environments, what are the potential values etc.

I’d argue that iff this is an internal configuration, you’d be better off with something like this:

(def my-service-config {:foo 1 :bar 2})

(do-something my-service-config some other params)

This way, it’s much easier to reason about the code, since you don’t have to run around chasing what the runtime values really are.

Reloadable config

An argument to keep (external) config in the Components is that this makes for reloadable config, ie you can restart your System, and you can inject new config into your running app. Back in the day when we deployed our apps once every quarter, this might have made sense, but today, where many of us deploy several times a day with zero downtime, it doesn’t really makes sense to introduce extra complexity to avoid restarting/redeploying a new version.

A parting example

We have some code to encrypt and sign stuff. This is dependent on our private key, which is part of our external configuration. Which makes sense. The functionality was implemented as a Component:

(defrecord CryptoService [rsa-private-key])

(defn encrypt [crypto-service s] 

So now, in order to encrypt something, you’d need a crypto-service which is something you’d get from your System:

(crypto-service/encrypt (:crypto-service @system) "sikr1t")

In a recent rewrite, I changed this into the following:

(defn rsa-private-key [config]
  (:rsa-private-key config))
(defn encrypt [rsa-private-key s]

so you’re able to encrypt something without the System, you only need a private key, and we’ve added a way to obtain that key if you happen to have a config, but you’re still able to invoke this fn even if you don’t have a config. The bigger upside to this is that crypto-service no longer need to be a Component, and our System has one less Component in it, and as such, has become a bit less complicated.