~ / track C / clojure ecosystem
Component, Mount, Integrant
IntermediateA real Clojure service has stateful resources: database connection pools, HTTP servers, message-queue consumers, scheduled-job runners. These need lifecycle management — start in the right order, stop in the reverse order, restart cleanly when code reloads. Three libraries own this space: Component (Stuart Sierra, 2014), Mount, and Integrant. Pick one early; switching later is painful.
Why not just def everything?
(def db (start-db config)) ;; runs at namespace load
(def app (handler db)) ;; coupled to a global
This works for a script. It breaks for a service:
- Tests have to reach in and
alter-var-rootto substitute a fake DB. - Reloading a namespace doesn't restart the DB; the old one leaks.
- Startup order is implicit (whatever order ns's get loaded).
- You can't run two instances side-by-side.
A component system gives you an explicit system value you can start, stop, and replace.
Component (Stuart Sierra)
(require '[com.stuartsierra.component :as component])
(defrecord Database [url conn]
component/Lifecycle
(start [this]
(assoc this :conn (open url)))
(stop [this]
(close conn)
(assoc this :conn nil)))
(defrecord Web [port db server]
component/Lifecycle
(start [this]
(assoc this :server (jetty/run-jetty (handler db) {:port port})))
(stop [this]
(.stop server)
(assoc this :server nil)))
(defn system [config]
(component/system-map
:db (->Database (:db-url config) nil)
:web (component/using
(->Web (:port config) nil nil)
[:db])))
(def sys (component/start (system config)))
(component/stop sys)
- Every stateful resource is a record implementing
Lifecycle. - The system map declares dependencies;
component/usinginjects the DB into the web component beforestart. - Start order is a topological sort of the dependency graph.
The flagship idea: dependencies are injected as fields of the record, not looked up from globals. Tests build a system with a fake DB record; production builds one with the real DB. Same code path.
Mount
(require '[mount.core :as mount :refer [defstate]])
(defstate db
:start (open (:db-url (mount/args)))
:stop (close db))
(defstate web
:start (jetty/run-jetty handler {:port 3000})
:stop (.stop web))
(mount/start)
(mount/stop)
- Each
defstateis a single var with:startand:stopthunks. - Mount tracks order by namespace load order.
- No explicit dependency graph; you reference other defstates as plain
vars (
dbabove).
Mount is simpler to start with, especially if you already have a codebase built around var references. Critics argue this is just namespaced globals with a lifecycle, and that it doesn't support multiple system instances or clean test substitution.
Integrant
(require '[integrant.core :as ig])
(def config
{:adapter/jetty {:port 3000 :handler (ig/ref :handler/main)}
:handler/main {:db (ig/ref :db/postgres)}
:db/postgres {:url "jdbc:postgresql://..."}})
(defmethod ig/init-key :db/postgres [_ {:keys [url]}]
(open url))
(defmethod ig/init-key :handler/main [_ {:keys [db]}]
(fn [req] (db-handler db req)))
(defmethod ig/halt-key! :db/postgres [_ conn]
(close conn))
(def sys (ig/init config))
(ig/halt! sys)
- The system is plain EDN data (often a
.ednfile). ig/refresolves at init to the actual built component.- Each lifecycle step is a multimethod keyed on the component's keyword.
- Wide adoption thanks to
ductandaerointegration.
How they compare
| Component | Mount | Integrant | |
|---|---|---|---|
| System representation | Record map | Vars in namespaces | EDN data |
| Dependencies | Explicit, in using | Implicit (var refs) | Explicit, ig/ref |
| Multiple instances | Yes | No (singleton vars) | Yes |
| Easy test substitution | Yes (build a different system) | Harder (alter-var-root) | Yes (build a different config) |
| Config file friendly | Manual | Manual | Native (read EDN, init) |
| Code intrusion | Records implement Lifecycle | defstate everywhere | None — multimethods external |
| Created by | Stuart Sierra | tolitius | weavejester |
| Sweet spot | Large services with clear modules | Small services, scripts | Config-driven services, duct ecosystem |
The "reloaded" workflow
All three work with clojure.tools.namespace.repl/refresh:
(require '[clojure.tools.namespace.repl :as tn])
(defn reset []
(component/stop sys)
(tn/refresh :after 'myapp.system/start))
In the REPL ([[repl-driven-development]]) you call (reset) and the
system stops, all changed namespaces reload, then a fresh system
starts. Iteration time goes from minutes (restart JVM) to seconds.
Try it (informational)
Real-world
| Pattern | Where |
|---|---|
Pass system around as ctx | All three libraries; handlers receive the system or its needed parts |
| Aero + Integrant for config | aero/read-config → ig/init is a common idiom |
Test fixtures call (component/start ...) with a fake DB | Mainstay of Component projects |
| Mount in CLI apps and Babashka scripts | Lightweight enough for one-off tools ([[babashka]]) |
| Donut.system | Newer entrant; data + cycle support + signals |
| Stuart Sierra's "Reloaded" workflow | Originated this whole pattern in 2013 |
| Replacing dynamic vars with system passing | The modern alternative to [[dynamic-vars-and-binding]] |
Check yourself
? quiz
A Web component depends on a Database component. In Component, how is the DB made available to the Web's `start`?
Exercise
Sketch a system with three components:
:config— reads a config map (no real lifecycle).:db— depends on:config, opens a connection on start.:web— depends on:db, starts a Jetty server.
In Component, write the system-map with using for the dependencies.
In Integrant, write the same as an EDN config with ig/refs.
Which is easier to swap the :db for a test fake? Which is easier
to keep in version-controlled config files? Both have a winner.