~ / track D / clojure in practice

Composing applications with components

Intermediatepractice

A 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:

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

Each handler then becomes a pure function that takes the system (or just the slice of it it needs) and the request:

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

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:

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

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, a mount.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 l‌e‌t ordering.

Among the popular choices:

  • Component (Stuart Sierra) — components are records that implement a Lifecycle protocol; the system map is a graph the framework topologically sorts.
  • Integrant — system is an :integrant.core/init-key multimethod 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 d‌e‌f. 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 p‌r‌i‌n‌t‌l‌n its lifecycle event so you can see the order at the REPL.

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