Skip to content

Replacing Integrant and Docker Compose with BigConfig System

system

In the Clojure ecosystem, we’ve grown accustomed to a structural divide. We manage our application architecture with libraries like Integrant and our external dependencies (databases, queues, caches) with Docker Compose or Process Compose .

While effective, this separation creates a constant “context switch.” Your application lives in the REPL, but its foundation is buried in YAML and EDN files. One requires evaluating expressions; the other requires multiple steps to refresh, start, or stop using Emacs or a Shell.

To bridge this gap, I’ve been developing BigConfig System. It is a library inside the BigConfig ecosystem that treats system lifecycles as programmable workflows rather than static dependency graphs.

I view BigConfig through the lens of the Emacs philosophy: the environment is something you should never have to leave.

As projects scale, we traditionally reach for external orchestrators. However, from the perspective of a running REPL, they have foreign interfaces. By keeping the orchestration logic strictly within Clojure I’ve unified the application and its dependencies. Now, I can manage the entire stack, from environment variables to database migrations, without ever dropping out of my REPL session.

To test the limits of BigConfig System, I moved away from static Postgres containers in favor of a dynamic, code-driven workflow. I needed to handle multiple profiles (like dev for long-running sessions and test for transient fixtures) simultaneously without port collisions or state pollution.

Instead of a binary “started/stopped” toggle, BigConfig manages a granular execution sequence:

  • Environment Prep: Dynamically set variables for project-root/.postgres/[profile].
  • Filesystem Cleanup: Ensure a clean slate by wiping stale data from previous runs.
  • Initialization: Programmatically execute initdb.
  • Smart Startup: Launch Posgres via babashka/process, grepping logs for the “ready to accept connections” regex.
  • Provisioning: Chain-load createuser, createdb, and sql-migrate.
  • Teardown: A dedicated stop-fn ensures the process tree is destroyed cleanly.

Many Clojure developers rely on cider-ns-refresh, but for a refined flow, I find it a bit heavy-handed. My development style centers on evaluating expressions directly within (comment ...) blocks.

By using cider-eval-defun-at-point and cider-inspect-last-result, I gain high-granularity control. The beauty of BigConfig is that the CIDER inspector updates automatically as the system state changes. I can see the system evolve in real-time, directly within my editor, without the jarring reset of a full namespace refresh.

Here is how BigConfig System expresses a managed process as a programmable workflow. Notice how the system lifecycle is defined as a series of step functions (background-process and stop):

(comment
(do
;; Define a managed background process with a specific lifecycle
;; Start the process in the background and block the main thread
;; until the regex is found or the timeout triggers.
(defn background-process [opts]
(let [cmd "bash -c 'for i in {10..1}; do echo $i; sleep 0.1; done;'"
regex #"7"]
(-> (re-program cmd regex ::proc opts)
(add-stop-fn (fn [{:keys [::proc] :as opts}]
(when proc
@(p/destroy-tree proc)
@(destroy-forcibly proc))
opts)))))
;; Define the system as a stateful, wired workflow
(def ->system
(->workflow {:first-step ::start
:wire-fn (fn [step _]
(case step
::start [background-process ::end]
::end [stop]))}))
;; sys1 starts and stops the system. It useful during the development of the system itself.
;; sys2 starts only. This is useful in all the other cases.
(into {} [[:sys1 (->system [log-step-fn] {::bc/env :repl})]
[:sys2 (let [system (atom (->system [log-step-fn]
{::bc/env :repl ::async true}))]
(stop! @system)
@system)]])))

emacs

Is it better than Integrant and Docker Compose?

Section titled “Is it better than Integrant and Docker Compose?”

It is still early days, and I am currently refining the library. One major advantage is that there is no longer a split between EDN, YAML, and CLJ files—it’s now just a single CLJ file. The shift in developer experience is already palpable. By replacing the fragmented mix of Docker Compose, Process Compose, and Integrant with a unified Clojure-based workflow, I’ve gained a level of introspection and speed that configuration-based tools and libraries simply cannot provide.

My quest to eliminate configuration files has reached a new milestone with system.edn and compose.yaml. By committing to a “never leave the REPL” philosophy, I’ve replaced traditional CLI tools and libraries with direct evaluation. While abandoning namespace reloading in favor of evaluating only the final expression isn’t for everyone, it has fundamentally transformed my productivity.

Would you like to have a follow-up on this topic? What are your thoughts? I’d love to hear your experiences.