~ / 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, Mount, and Integrant. Pick one early; switching later is painful.

Why not just d‌e‌f 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. The tradeoff: because each defstate is a singleton var, you can't run two systems side-by-side in the same JVM, and test substitution falls back to alter-var-root or w‌i‌t‌h‌-‌r‌e‌d‌e‌f‌s rather than building an alternate system value.

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)

The in-browser REPL can't load the real Component library, so the snippet below defines its own tiny Lifecycle protocol just to illustrate the shape — start returns an updated value of this, stop tears state down. The real com.stuartsierra.component adds the dependency graph and topological start order on top of exactly this idea.

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 (2022+) Integrant-style library; adds dependency cycles, signals beyond start/stop, and richer validation — pick it if Integrant feels too bare
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