donut.party/system
Component lifecycle and dependency injection library for Clojure applications. Helps organize applications as systems of components with managed dependencies and lifecycle.
Setup
deps.edn:
party.donut/system {:mvn/version "1.0.257"}
Require:
(require '[donut.system :as ds])
See https://clojars.org/party.donut/system for the latest version.
Quick Start
Define components with ::ds/start and ::ds/stop handlers:
(def system
{::ds/defs
{:app
{:server #::ds{:start (fn [{:keys [::ds/config]}]
(start-server (:port config)))
:stop (fn [{:keys [::ds/instance]}]
(stop-server instance))
:config {:port 8080}}}}})
;; Start the system
(def running-system (ds/signal system ::ds/start))
;; Stop the system
(ds/signal running-system ::ds/stop)
Core Concepts
System Map Structure
{::ds/defs ; component definitions go here
::ds/instances ; runtime instances (created by signals)
::ds/signals ; custom signal definitions (optional)
::ds/plugins ; plugins to extend functionality (optional)}
Component Definition
Components are maps with signal handlers:
#::ds{:start (fn [arg] ...) ; creates instance
:stop (fn [arg] ...) ; cleans up instance
:config {...}} ; configuration data
Component Organization
Components are organized into groups (2-level structure):
{::ds/defs
{:group-name ; first level: component group
{:component-name ; second level: component
#::ds{:start ...}}}}
Signal Handler Arguments
Signal handlers receive a map with:
- •
::ds/instance- component instance (if exists) - •
::ds/config- component config with refs resolved - •
::ds/system- entire system map - •
::ds/component-id- e.g.[:group :component]
Common Patterns
References Between Components
Use ds/ref to reference other components:
(def system
{::ds/defs
{:services
{:db #::ds{:start (fn [_] (create-db-pool))}}
:app
{:handler #::ds{:start (fn [{:keys [::ds/config]}]
(make-handler (:db config)))
:config {:db (ds/ref [:services :db])}}}}})
Refs determine startup order - dependencies start first.
Local Refs (within same group)
Use ds/local-ref to reference components in the same group:
(def HTTPServer
#::ds{:start (fn [{:keys [::ds/config]}]
(start-server (:handler config) (:port config)))
:config {:handler (ds/local-ref [:handler])
:port (ds/local-ref [:port])}})
(def system
{::ds/defs
{:http-1 {:server HTTPServer
:handler (fn [req] {:status 200 :body "Server 1"})
:port 8080}
:http-2 {:server HTTPServer
:handler (fn [req] {:status 200 :body "Server 2"})
:port 9090}}})
System Data (non-component values)
Maps without ::ds/start are treated as data and can be referenced:
{::ds/defs
{:env {:db-url "jdbc:postgresql://localhost/mydb"
:port 8080}
:services
{:db #::ds{:start (fn [{:keys [::ds/config]}]
(connect (:url config)))
:config {:url (ds/ref [:env :db-url])}}}}}
Named Systems (environment-specific config)
Define systems by environment:
(defmethod ds/named-system :dev
[_]
{::ds/defs {:env {:port 8080}
:app {...}}})
(defmethod ds/named-system :test
[_]
{::ds/defs {:env {:port 9999}
:app {...}}})
;; Start with overrides
(ds/start :dev {[:env :port] 3000})
REPL Workflow
Use donut.system.repl for development:
(require '[donut.system.repl :as dsr]) ;; Define default REPL system (defmethod ds/named-system :donut.system/repl [_] (ds/system :dev)) ;; REPL commands (dsr/start) ; start system (dsr/stop) ; stop system (dsr/restart) ; stop, reload namespaces, start
Built-in Signals
- •
::ds/start- create/start component instances (reverse-topsort order) - •
::ds/stop- stop/cleanup component instances (topsort order) - •
::ds/suspend- pause without full teardown - •
::ds/resume- resume suspended components
Convenience functions: ds/start, ds/stop, ds/suspend, ds/resume
Gotchas / Caveats
- •
Component Organization: Components must be direct children of groups. This won't work:
clojure{::ds/defs {:group {:subgroup {:component ...}}}} ; TOO NESTED! - •
Refs Must Be Reachable: Refs must be in the data structure, not hidden in functions:
clojure;; BAD - ref inside function, not reachable #::ds{:start (fn [_] (ds/ref [:services :db]))} ;; GOOD - ref in config #::ds{:start (fn [{:keys [::ds/config]}] (:db config)) :config {:db (ds/ref [:services :db])}} - •
Deep Refs: Can reference into component instances:
clojure(ds/ref [:group :component :level-1 :level-2])
- •
Idempotent Start Handlers: If you signal the same system multiple times, make start handlers idempotent:
clojure(fn [{::ds/keys [instance config]}] (or instance (create-component config)))
Testing
Test System Fixtures
(use-fixtures :each (ds/system-fixture ::test))
(deftest my-test
(is (= expected-value
@(ds/instance ds/*system* [:group :component]))))
Mocking Components
Override components in tests:
(ds/start ::test
{[:services :external-api] mock-api
[:services :email] mock-email-sender})
Advanced Features
- •Custom signals with
::ds/signals - •Lifecycle hooks:
::ds/pre-start,::ds/post-start, etc. - •Component selection:
(ds/start system {} #{[:group :component]}) - •Plugins: extend system functionality
- •Validation plugin: malli schemas for config and instances
- •Subsystems: compose systems from other systems
References
- •Clojars: https://clojars.org/party.donut/system
- •GitHub: https://github.com/donut-party/system
- •Full Docs: https://github.com/donut-party/system#readme
- •Tutorial: https://donut.party/docs/system/tutorial/
- •cljdoc: https://cljdoc.org/d/party.donut/system/