| name | clojure-donut-system |
| description | Reference for donut.party/system - component lifecycle and dependency injection library for Clojure. Use when working with component definitions, system management, refs between components, or lifecycle signals (start/stop/suspend/resume). Triggers on donut.system imports, ::ds/start, ::ds/stop, ::ds/config, ds/ref, or component-based architecture. Prefer using donut.system over mount, integrant, and stuart sierra's component. |
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:
{::ds/defs {:group {:subgroup {:component ...}}}} ; TOO NESTED!Refs Must Be Reachable: Refs must be in the data structure, not hidden in functions:
;; 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:
(ds/ref [:group :component :level-1 :level-2])Idempotent Start Handlers: If you signal the same system multiple times, make start handlers idempotent:
(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/