~ / track C / clojure ecosystem

Component, Mount, Integrant

Intermediate

A 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-root to 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/using injects the DB into the web component before start.
  • 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 defstate is a single var with :start and :stop thunks.
  • Mount tracks order by namespace load order.
  • No explicit dependency graph; you reference other defstates as plain vars (db above).

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 .edn file).
  • ig/ref resolves at init to the actual built component.
  • Each lifecycle step is a multimethod keyed on the component's keyword.
  • Wide adoption thanks to duct and aero integration.

How they compare

ComponentMountIntegrant
System representationRecord mapVars in namespacesEDN data
DependenciesExplicit, in usingImplicit (var refs)Explicit, ig/ref
Multiple instancesYesNo (singleton vars)Yes
Easy test substitutionYes (build a different system)Harder (alter-var-root)Yes (build a different config)
Config file friendlyManualManualNative (read EDN, init)
Code intrusionRecords implement Lifecycledefstate everywhereNone — multimethods external
Created byStuart Sierratolitiusweavejester
Sweet spotLarge services with clear modulesSmall services, scriptsConfig-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)

loading sci
press ⌘/Ctrl-↵ or click ▶ run to evaluate

Real-world

PatternWhere
Pass system around as ctxAll three libraries; handlers receive the system or its needed parts
Aero + Integrant for configaero/read-configig/init is a common idiom
Test fixtures call (component/start ...) with a fake DBMainstay of Component projects
Mount in CLI apps and Babashka scriptsLightweight enough for one-off tools ([[babashka]])
Donut.systemNewer entrant; data + cycle support + signals
Stuart Sierra's "Reloaded" workflowOriginated this whole pattern in 2013
Replacing dynamic vars with system passingThe 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:

  1. :config — reads a config map (no real lifecycle).
  2. :db — depends on :config, opens a connection on start.
  3. :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.

 status: new