The purpose of this article is to demonstrate how to build web applications using the Duct framework.

In a nutshell, Duct is a state and configuration manager. By state, I mean stateful objects like database connections, event listeners (such as Kafka and SQS), caches, websocket connections, loggers, etc. Configuration, on the other hand, refers to the relationships between these stateful objects.

Take a look at the simple todo app as an example.

Initial project setup

Starting from scratch

In our todo app, we will have a few stateful objects (let's call them components): a DB connection and an HTTP server. Without Duct, it might look like the following sample. You can define components as global variables and manage their lifecycle manually:

(ns todo.core
  (:require
   [next.jdbc :as jdbc]
   [ring.adapter.jetty :as jetty])
  (:import
   [java.sql Connection]
   [org.eclipse.jetty.server Server]))

;; init
(def ^Connection connection
  (jdbc/get-connection
   {:connection-uri "jdbc:sqlite::memory:"
    :dbtype         "sqlite"}))

;; sutdown
(.close connection)

;; init
(def ^Server server
  (jetty/run-jetty
   (fn [request]
     "business logic goes here")
   {:port  3000
    :join? false}))

;; shutdown
(.stop server)

As you can see, there are two lifecycle phases for each component: initialization and shutdown. Let's use Duct to wrap them in a proper lifecycle hook.

Introducing Integrant

Duct is built on top of the integrant library which provides a couple of multimethods to hook up our components: init-key and halt-key!

Let’s create a src/todo/core.clj file

(ns todo.core
  (:require
   [next.jdbc :as jdbc]
   [ring.adapter.jetty :as jetty]
   [integrant.core :as ig])
  (:import
   [java.sql Connection]
   [org.eclipse.jetty.server Server]))

(defmethod ig/init-key ::connection [_ config]
  (jdbc/get-connection
   {:connection-uri "jdbc:sqlite::memory:"
    :dbtype         "sqlite"}))

(defmethod ig/halt-key! ::connection [_ ^Connection connection]
  (.close connection))

(defmethod ig/init-key ::server [_ config]
  (jetty/run-jetty
   (fn [request]
     "business logic goes here")
   {:port  3000
    :join? false}))

(defmethod ig/halt-key! ::server [_ ^Server server]
  (.stop server))

Usually you’ll want things like port, DB url and other secrets to come from the configuration map rather than hardcoding them into the source code. So let’s refactor our app a bit: