May 28, 2019 • Arnout Roemers
The reloaded workflow; I think every substantial Clojure projects needs it. There are various libraries available for this lifecycle management, such as Component, Integrant and mount-lite. While I’m a big fan of the easy approach by the latter, some projects are better suited for a more functional approach - passing the system to functions - such as Component.
However, I tried working with Component multiple times, but every time it didn’t feel quite right. On a recent project of mine, I experimented on how to deal with lifecycle management, and I stumbled upon a solution that seems to fix the issues I had.
Firstly, you need a record to define your component.
Lifecycle protocol works fine for me, but with Component it also needs to be implemented on a record, as that’s how dependencies are injected.
That’s already a bit of overhead, as opposed to using an inline
reify for example.
But, the started component also needs to stay/be an object implementing the
Most likely, this is the updated record itself, so the component can access its dependencies when stopping.
The downside of this is that the actual component might very well be inside the record.
Think of a JDBC connection pool component for example.
To be able to stop such a component, it needs to implement the
You either need to wrap the connection pool inside the record, or you need to
Most likely the first option is chosen.
For me this is a major downside, as now all the other components need to know that the actual connection pool must be retrieved from somewhere inside the component.
It feels like having an abstraction where you don’t need or want one.
By the way, Integrant improves upon this, but in some cases it has the same issue.
Secondly, the recommended way of creating a component composition in Copmonent is by using its
This takes key-value pairs, together with dependency declarations with
In the end, the result is just a map with metadata on its values.
But that was not clear from the API; I learned this by reading the source code.
Knowing this, I was able to define different parts of the system map at different places - something I wanted for my current Clojure project - as you can simply merge the system maps.
Still, this merging must be done explicitly, tightly coupling those places.
Thirdly, I don’t really care for defining the exact dependencies between components up-front. Often I am just fiddling in the system-map, to get everything to work together. Since the component composition is defined in one place, it is clear I already know the dependencies. In that case I can just as easily put the components in the correct order myself, right?
So, let’s change a small thing about how Component and its
Instead of having the library build up the system map when starting the composition, have the components do it.
A component takes the system map built so far, starts itself, and adds itself to the system map.
Now the next component can be started.
And when stopping the component, the system map is passed in as well.
Firstly, this allows you to add any type of component to the system map. A function, a JDBC connection pool, a protocol implementation, anything.
Secondly, a component gets to add multiple entries to the system map, if it so desires. This way, one “component” could add multiple other components, transparently. Or, if the component needs to store something it needs for its stopping logic (other then its dependencies), you could also add this. No wrapping of the actual component needed.
Thirdly, this approach does not need a dependency tree. Starting the system is simply a reduction over the start functions.
Below is the source code of everything that’s needed for this approach. Note that the namespaces are fictitious.
combine function above is a utility function, starting/stopping multiple Lifecycle implementations in the order the are passed in.
Below is an example application on how to use it.
Because the components are in charge of how to add entries to the system map, it can also easily be extended with other behaviour. For example, another utility function could have parts of the system start up in parallel:
Now imagine that both the DB components from the example app are independent of each other.
And let’s say that they perform migrations on startup.
In that case we may improve our startup time by using this
parallel function as follows:
Well, there you have it.
It turns out the small
lifecycle namespace above is all I need for the project, and it feels right.
It does not have features like only starting/stopping parts of the system, or declaring the component composition using a datastructure.
I currently don’t need those features, yet they are easily done in a REPL.
Keep in mind, all this is very much my own opinion. I don’t want to pick upon the great work that was - and still is - being done by the community on this subject. I just wrote it down for those who might have had the same issues and may find my solution useful.
As always, have fun!
P.S. Here’s a link to the source code