~ / track D / clojure in practice
Composing applications with components
IntermediatepracticeA small program is one namespace and a -main. A real program is a
system — a handful of long-lived things (a database connection pool, a
job queue, an HTTP server, a metrics reporter) that need to start in a
specific order, hold state, and shut down cleanly when the process exits.
This lesson is about the patterns Clojure programs use to compose those
pieces without resorting to globals. Frameworks like Component, Mount, Integrant
formalize the same pattern; Web stack: Ring and Reitit
shows where handlers sit in the request lifecycle; and Organizing application state
covers how many reference cells your running service needs.
The shape of the problem
Without discipline, dependencies leak into top-level vars:
(def db (jdbc/get-datasource db-config))
(defn list-users []
(jdbc/execute! db ["select * from users"]))This works until you want to test list-users against a fake database, or
spin two systems up side-by-side, or restart the database without restarting
the JVM. The fix is the same in every language: stop reaching for
globals; pass dependencies in.
The pattern: a system map
A system is a plain map from component name to the thing itself:
Each handler then becomes a pure function that takes the system (or just the slice of it it needs) and the request:
No global ever appears. Tests pass {:db (mock-pool)}; production passes
the real pool; both code paths are identical.
Start order matters
Real components have dependencies. The metrics reporter needs the registry; the HTTP server needs the handlers, which need the database. You want a single function that starts everything in dependency order and another that stops everything in reverse.
The hand-rolled version is just two functions:
That's the kernel of every component framework in the ecosystem. Stuart Sierra's Component, Weavejester's Integrant, and Tolitius's Mount all wrap this same idea with extra machinery — lifecycle protocols, dependency declarations read from a config file, REPL-friendly restart — but the underlying pattern is identical.
REPL-driven workflow
Once start and stop are functions of data, the whole system becomes a
value you can swap at the REPL:
(defonce sys (atom nil))
(defn go [] (reset! sys (start (read-config))))
(defn halt [] (when @sys (swap! sys stop)))
(defn reset []
(halt)
(require :reload-all 'my.app.core)
(go))reset reloads the changed code, restarts the system, and you're back at a
known state in under a second. This is the workflow that makes "live coding
production" feel safe — the boundary between dev and a running server is a
function call you can read.
When to upgrade to a framework
A hand-rolled start/stop carries you a long way. Reach for a framework
when one of these starts hurting:
- You want to describe the system as data (an
integrant.edn, amount.edn) rather than code. - You want to start a subset of the system (just the db, for a one-off migration script).
- You want the dependency graph computed from declarations rather than encoded in your letclojure.core/letLocal bindings: (let [k v …] body).view on clojuredocs → ordering.
Among the popular choices:
- Component (Stuart Sierra) — components are records that implement a
Lifecycleprotocol; the system map is a graph the framework topologically sorts. - Integrant — system is an
:integrant.core/init-keymultimethod per component; the config is plain edn, easy to override per environment. - Mount — each component is a
defstate; less ceremony, more global, trade-off chosen consciously.
All three solve the same problem. Pick one and stop debating it — the choice matters less than the discipline of having a single, restartable system value.
In a Clojure service
Production services wire dependencies once at startup and pass slices of the
system map into handlers. The HTTP server holds {:keys [handlers]}; each
handler destructures only what it needs — typically {:keys [db]} — and
never reaches for a top-level defclojure.core/defBind a name to a value in the current namespace.view on clojuredocs →. Tests call the same handler with
{:db :fake-pool}; production passes the real pool. start builds the map
in dependency order; stop tears it down in reverse. No globals, no hidden
coupling.
(defn list-orders-handler [{:keys [db]} _request] ;; sketch
{:status 200 :body (query-orders db)})
(defn ->handlers [system]
{:list-orders (partial list-orders-handler system)})
(defn start [config]
(let [db (start-pool! (:db-url config))]
{:db db :handlers (->handlers {:db db})}))Check yourself
? quiz
What does passing the system map into a handler buy you over reaching for a top-level `def`?
Exercise
Sketch a start and stop for a tiny system with three components:
:config, :cache, and :server. The cache depends on the config; the
server depends on both. Have each function printlnclojure.core/printlnPrint arguments separated by space, then newline.view on clojuredocs → its lifecycle event so
you can see the order at the REPL.