A lot of people have already heard that they should use Stuart Sierra's component library to manage state in their Clojure applications. But most don't know why and how you would incorporate it into your software.
This how to guide will give you all the tools you need to start managing state in your Clojure software.
What is component?
Component is a Clojure library that can be thought of as a micro dependency injection framework. I use it in all my web projects to manage runtime state.
What are alternative(s) to component?
Mount is an alternate way of managing state in Clojure that's picking up steam.
Component enables the reloaded workflow, which makes it much easier to reload code with runtime state in a REPL. Imagine you have a web server running and you change a route and need to reload it. If you were just use a def, you can easily lose reference to that web server when reloading your namespace. Then you'd have a random server that you can't shut down in the background bound to a port that you can't use anymore.
Managing dependencies between components in your system. A web server component is going to need access to its handlers as a component. Some of those handlers are going to need access to a database component.
Forbids circular dependencies between components.
You can have multiple systems. Component, unlike mount, allows you to have as many systems as you want.
Components are reusable. You can create a database component one time that takes a config map and can connect to whatever database(s) you need.
Everything is a map. Your system-map is a map, and components are records that implement Lifecycle, which are also maps. It makes it easy to manipulate. For instance, you can have your application's system map in your tests, and instead of using an actual database connection, you can assoc a mock database instead to use.
You have to buy completely into component. If you already have an existing application, adding component to it will be tough because you needed to have designed your system in terms of components from the ground up.
You have to specify your components' dependencies manually.
Stuart Sierra says it might be overkill for small applications. Based on my own experience, I say it depends.
Component makes it so you have to pass (inject) your dependencies everywhere. Every function that touches your database is going to need to be passed in that database component somewhere. In general, I believe this is a good practice. If you would rather reference a singleton instead, you're not going to like component.
An Example Of Component
Let's say you're planning on building a new service for managing emails in your application. The work involved is taking messages off of a queue, doing some work, writing data to a database, and then putting things onto other queues. Everything will be asynchronous.
Imagine we had a queue that had requests for messages that needed to go out. An outgoing queue. You have other services that put things onto this queue when they need to send an email. You might have your website's user authentication functionality queue up emails to go out when a user signs up or forgets their password. You might also have a background job that queues up product recommendations to go out to users every week.
The job of this service is to take those messages, send off a request to an email service provider, log the messages and their statuses in a database, and queue up messages for other services to consume.
How would you break this down into components?
These are the components I see us needing:
Core async channels
Email service component
The database and queue should be super obvious candidates for componetizing. The worker process is basically a handler. The core async channels and email service are probably not so obvious components.
Let's sketch out some code. I'll explain as we go.
Note: This will just be snippets of code that I haven't fully tested as I don't plan on writing an entire microservice. If you find major errors, go ahead and send me an email.
Tips and Tricks
I've seen projects with code like this:
Component provides a no-op implementation of the Lifecycle protocol on all java objects. What that means is, if you need to provide an atom, map, or something else as a dependency, there's no need to make a record for it.
Just put it in your system map as-is.
If you're wondering why I assoc a nil instead of using dissoc, it's because dissoc returns a map and assoc keeps our record type.