Using component for dependency injection in clojure web apps

In clojure, the common dependency injection library is component. It allows naming services in your application. Then a service can declare dependencies on other services by those names. These services can also declare start and stop functions to properly initialize and cleanup. When the entire system is started, it will resolve those names and inject the correct dependencies where needed.

There are some good videos about component from recent conferences:

Hooking component up into a ring app

One of the issues that comes up with ring apps is the declaration of a ring handler as a static top level form. A common pattern in clojure web apps is to see

( defroutes routes .... ) ( def app ( -> routes middleware1 .... ))

This works fine for small apps and examples. However, app is built at compile time. There is no way to tell the ring handler to use a in-memory database or a storing email mechanism for the test environment, while using other ones for a development or production environment.

One solution is to turn app into a function that takes the map of services. Then it could add a middleware that adds the services to the ring request. Destructuring in routes can then pull out the services that are wanted. For example:

( defroutes routes ... ( PUT "/signup" { :keys [ services params ]} ( let [{ :keys [ database notifier ]} services { :keys [ user ]} params ] ( controller/signup-user db notifier user ))) ... ) ( defn wrap-services [ f services ] ( fn [ req ] ( f ( assoc req :services services )))) ( defn app [ services ] ( -> routes ( wrap-services services ) ... ))

Now services can be added when the handler is created. For tests, a library like kerodon can be given a handler with just test services.

But how does this work with component for the main application? Since component is used to start systems it could be used to start the database, mailer, and even the web server. The web server can declare a dependency on those services and create the service map it uses to create the handler.

( defrecord WebServer [ config jetty database notifier ] component/Lifecycle ( start [ this ] ( if jetty this ( assoc this :jetty ( jetty/run-jetty ( app { :database database :notifier notifier }) config )))) ( stop [ this ] ( when jetty ( .stop jetty )) ( assoc this :jetty nil )))

( defn system [ config ] ( component/system-map { :database ( database-component config ) :notifier ( email-component config ) :server ( component/using ( map->WebServer { :config config }))} [ :database :notifier ]))