October 1, 2025

Domain Driven Design in Clojure with generalized Hiccup

In this blog post, we present a general framework for Domain Driven Design in Clojure that addresses known shortcomings of Hexagonal architecture. In particular, we leverage the Free-er monad algebraic structure to build a Hiccup-like embedded domain-specific language parametrized by an arbitrary domain.

Michal Hadrava
Full-stack developer

Introduction

Software is but a tool to fulfill a need. It does not grow on trees; energy must be spent to build and maintain it. Moreover, the need itself, or at least the understanding thereof, evolves over time. The challenge is to minimize the energy spent on ensuring the software fulfills the need over its entire lifecycle.

There are different strategies to tackle the challenge; one of them is closing the semantic gap between the source code and the requirements; indeed, in the ideal case when the code is but a transcription of the requirements, ensuring its correctness in the face of changes to the requirements is trivial. To move closer to this nirvana, one can opt for a high-level programming language like Clojure; however, even Clojure code can at times fail to express the requirements clearly; we need the right approach in addition to the right language.

Among the approaches promising to reduce the semantic gap between the code and the requirements, Domain-driven design (DDD) is particularly prominent; in this approach, the structure of the code is dictated by a model of the business domain. Still, how exactly one lets the model dictate the structure of the code has repercussions for the development and maintenance costs as well; these days, one would typically go with Hexagonal architecture (Hexagon).

A typical “hexagonal” Clojure codebase is split into three parts:

  1. Domain
  2. Ports
  3. Adapters

Domain is a translation of the domain model to code which calls protocol methods from Ports; the methods map directly to the operations from the domain model; Adapters implement the protocols from Ports. Here, Ports is the key ingredient; it frees Domain from implementation concerns so that the code living in it can match the domain model closely; care must be taken not to leak the implementation structure to Domain through grouping of the methods into protocols, though; ideally, each method should have its own protocol. Moreover, in a typical “hexagonal” codebase, it is tempting to bypass Ports altogether and use an Adapter directly; it is both easy to do and difficult to spot. On the other hand, with Domain only calling protocol methods from Ports, each Domain test needs an Adapter injected for each Port it touches; typically, this entails implementing mocks for all the Ports; consequently, Domain tests only tell as much about the correctness of the system as closely the mocks match the “real” Adapters. The question is: can we do better?

Embedded domain-specific language as an alternative to Hexagon

Whereas Hexagon is a common DDD design pattern in object-oriented languages (and Clojure), purely functional languages like Haskell have different idioms like, for instance, embedded domain-specific language (eDSL) - typically leveraging the Monad, Free monad, or even Free-er monad algebraic structure. In general, DDD using the eDSL pattern entails

  1. Identifying a set of abstract atomic operations in the domain model
  2. Defining combinators for the operations
  3. Defining interpreters for expressions formed by applying the combinators to the operations

We call the operations “atomic” because they are irreducible in the context of the domain model; they correspond to protocol methods in a Clojure Hexagon; however, in contrast to the latter, they are just data; they are “words” of a domain-specific language. The combinators make it possible to re-create the domain model in code using the abstract atomic operations as building blocks; they define the “syntax” of the domain-specific language. Finally, the interpreters provide alternative implementations of the domain model; they correspond to Adapters in Hexagon; however, with a careful selection of the combinators (e.g. Monad operations), ad-hoc dependency injection can be replaced by an algorithmic transformation of eDSL expressions; remarkably, the transformation is domain-independent, parametrized by a map from abstract atomic operation to its implementation.

As argued elsewhere, the eDSL approach addresses the shortcomings of Hexagon outlined above; namely, the domain model being but an eDSL expression to be interpreted, calls to operation implementations outside the interpretation context are impossible; by the same token, the tests can inspect the domain model directly (it is just data!) instead of having to rely on mock implementations.

In the following section, we present a general framework for applying the eDSL approach in Clojure.

eDSL in Clojure with generalized Hiccup

We present our approach by building an eDSL for a domain consisting of a single abstract atomic operation - inc; the operation takes a number as an input and returns the same number incremented by one. To build the eDSL, we proceed in the three steps outlined above; with the abstract atomic operations already identified (we only have one - inc), we proceed directly to the second step - defining the combinators.

We define the combinator(s) by translating the Free-er monad construction from Haskell to Clojure, leveraging a generalization of Hiccup. In Haskell, the Free-er monad is constructed as follows:

data FFree f a where
  Pure :: a -> FFree f a
  Impure :: f x -> (x -> FFree f a) -> FFree f a

instance ... Monad (FFree f) where
  ...
  Pure a >>= k = k a
  Impure fx k' >>= k = Impure fx (\x -> k' x >>= k)

Here, FFree f a is a generic data type, Pure and Impure its constructors (followed by their respective type signatures), and FFree f is the Free monad; more precisely, FFree f is an instance of Monad with the >>= combinator defined as above. Intuitively, the Pure constructor wraps a value of type a while the Impure constructor connects an operation with output type x to a function with the matching input type; we call the function “continuation” as it specifies how to continue once the output from the operation is available.

Let us first translate construction of a value of type FFree f a to a Hiccup expression; more precisely, we need two types of expressions: one for Pure and another for Impure; then, counting from zero to one can be modelled as:

(def count-to-one
 [:impure
  [:inc 0]
  (fn [one]
   [:pure one])])

Here, :impure maps to the Impure constructor, [:inc 0] to the operation, and (fn [one] …) to the continuation. As to the operation, [:inc 0] represents the inc abstract operation being called with 0 as the input. As to the continuation, :pure maps to the Pure constructor and one to the value being wrapped. 

Now, translating the implementation of >>= for FFree f is straightforward:

(defn >>=
 [[head & _ :as ffree] k]
 (case head
  :pure
  (let [[_pure a] ffree]
   (k a))

  :impure
  (let [[_impure fx k'] ffree]
   [:impure
    fx
    (fn [x]
     (>>= (k' x) k))])))

In order to get an intuition of what >>= actually does, let us trace the evaluation of the following expression:

(>>=
 [:impure
  [:inc 0]
  (fn [one]
   [:pure one])]
 (fn [one]
  [:impure
   [:inc one]
   (fn [two]
    [:pure two])]))

Taking the :impure branch in >>=, we compose the continuation of the first operand with the second operand by calling the former and then feeding the result to the latter with >>=:

[:impure
 [:inc 0]
 (fn [one]
  (>>=
   [:pure one]
   (fn [one]
    [:impure
     [:inc one]
     (fn [two]
      [:pure two])])))]

Now, taking the :pure branch in the recursive call to >>=, we simply unwrap the value from the first operand and feed it to the second operand:

[:impure
 [:inc 0]
 (fn [one]
  [:impure
   [:inc one]
   (fn [two]
    [:pure two])])]

Comparing the result to the original expression:

(>>=
 [:impure
  [:inc 0]
  (fn [one]
   [:pure one])]
 (fn [one]
  [:impure
   [:inc one]
   (fn [two]
    [:pure two])])))

one can see that >>= is a means to feed output of one operation as an input to another. Moreover, as Monad is a subclass of Applicative and Functor, the <$> and <*> combinators can be defined which make it possible to compose multi-input operations as well (see the example here); however, we do not pursue this avenue here and instead introduce a more idiomatic multi-input composition later. Last but not least, any business logic can be embedded in a continuation as long as it only depends on the input and evaluates to either [:pure …] or [:impure …].

As a final step towards an eDSL for our micro-domain, we define the interpreter(s). This is quite straightforward; all we need is to evaluate each [:inc …] expression inside [:impure …] and apply the continuation to the result, recursively:

(defn interpreter
 [impl [head & _ :as ffree]]
 (case head
 :pure
 (let [[_pure a] ffree]
  a)
 
 :impure
 (let [[_impure [op & op-args] k'] ffree
       op-impl (get impl op)]
  (interpreter impl (k' (apply op-impl op-args))))))

Here, impl maps an operation to its implementation; in our domain, it could be one of the following:

(def calculator
 {:inc inc})

(def printer
 {:inc #(str % " + 1")})

Note that while the interpreter handles  [:pure …] and [:impure …] expressions in a special way, their shape is no different from that of [:inc …] expressions. This observation motivates including :pure and :impure as additional operations in the implementation maps:

(def ffree
 {:pure identity
  :impure #(%2 %1)})

(def calculator
 (-> {:inc inc}
     (merge ffree)))

(def printer
 (-> {:inc #(str % " + 1")}
     (merge ffree)))

and modifying the interpreter so that it evaluates the [:impure …] expressions correctly:

(defn interpreter
 [impl x]
 (if-not (vector? x)
   x
   (let [[head & tail] x
         op-impl (get impl head)]
     (if-not op-impl
      x
      (->> tail
           (map (partial interpreter impl))
           (apply op-impl)
           (interpreter impl))))))

To see that the new interpreter evaluates [:pure …] and [:impure …] expressions in the same way as the original one, let us trace the evaluation of the following expression:

(interpreter
 calculator
 [:impure
  [:inc 0]
  (fn [one] [:pure one])])

(interpreter
 calculator
 (apply
  #(%2 %1)
  [(interpreter calculator [:inc 0])
   (interpreter calculator (fn [one] [:pure one]))]))

(interpreter
 calculator
 (apply
  #(%2 %1)
  [(interpreter calculator (apply inc [(interpreter calculator 0)]))
   (interpreter calculator (fn [one] [:pure one]))]))

(interpreter
 calculator
 (apply
  #(%2 %1)
  [(interpreter calculator (apply inc [0]))
   (interpreter calculator (fn [one] [:pure one]))]))

(interpreter
 calculator
 (apply
  #(%2 %1)
  [(interpreter calculator 1)
   (interpreter calculator (fn [one] [:pure one]))]))

(interpreter
 calculator
 (apply
  #(%2 %1)
  [1
   (interpreter calculator (fn [one] [:pure one]))]))

(interpreter
 calculator
 (apply
  #(%2 %1)
  [1
   (fn [one] [:pure one])]))

(interpreter
 calculator
 [:pure 1])

(interpreter
 calculator
 (apply identity [(interpreter calculator 1)]))

(interpreter
 calculator
 (apply identity [1]))

(interpreter calculator 1)

1

Note that we have silently introduced support for multi-input operation composition. Also note that :pure is but identity and :impure but reverse function application; consequently, they can be eliminated from the eDSL (and implementation maps) like this:

[:impure
 [:inc 0]
 (fn [one]
  [:impure
   [:inc one]
   (fn [two]
    [:pure two])])]

[:impure
 [:inc 0]
 (fn [one]
  [:pure
   [:inc one]])]

[:impure
 [:inc 0]
 (fn [one]
  [:inc one])]

[:inc
 [:inc 0]]

Unfortunately, the reduction above breaks if the body of the continuation is not a Hiccup expression; like, for instance, when the continuation contains some business logic:

[:impure
 [:inc 0]
 (fn [number]
  (if (< number 2)
   [:impure
    [:inc number]
    (fn [two]
     [:pure two])]
   [:pure number]))]

[:impure
 [:inc 0]
 (fn [number]
  (if (< number 2)
   [:pure
    [:inc number]]
   [:pure number]))]

[:impure
 [:inc 0]
 (fn [number]
  (if (< number 2)
   [:inc number]
   [:pure number]))]

To remedy this, we rewrite the continuation:

(defn inc-if-needed
 [number]
 (if (< number 2)
  [:inc number]
  [:pure number]))

[:impure
 [:inc 0]
 (fn [number]
  [inc-if-needed number])]

and extend the interpreter with support for fn? vector heads:

(defn interpreter
 [impl x]
 (if-not (vector? x)
   x
   (let [[head & tail] x
         op-impl (or (and (fn? head) head) (get impl head))]
     (if-not op-impl
      x
      (->> tail
           (map (partial interpreter impl))
           (apply op-impl)
           (interpreter impl))))))

Finally, the reduction can proceed as before:

[:impure
 [:inc 0]
 (fn [number]
  [inc-if-needed number])]

[inc-if-needed
 [:inc 0]]

Our eDSL is now finished, furnished with an interpreter parametrized by an implementation map; the eDSL consists of generalized Hiccup expressions, each tagged with either an abstract operation from the domain or a function returning an eDSL expression.

Conclusion

Having recounted the shortcomings of Hexagonal architecture, we explored embedded domain-specific language as an alternative approach to Domain Driven Design in Clojure. In particular, having first translated the Free-er monad algebraic structure to Hiccup-like expressions, we were in position to derive an eDSL for a toy domain in conjunction with an interpreter for the eDSL. Notably, the interpreter is generic; it can evaluate any Hiccup-like eDSL expression as long as it is tagged with either

  • an abstract atomic operation from the domain in question or
  • a function returning such a Hiccup-like eDSL expression (typically based on some business logic)

and each abstract atomic operation from the domain in question has a corresponding implementation in the map passed to the interpreter. Let us emphasize, though, that no interpreter and hence no implementation map is needed for domain tests; indeed, with business logic implemented as functions returning eDSL expressions, the latter can be inspected directly, without any evaluation.

Related work

Even though Hexagonal architecture is a dominant approach to DDD in Clojure, eDSL’s thrive in specific areas like web development (hiccup, re-frame, replicant), data validation and coercion (malli), database access (honeysql), or architecture (integrant). Notably, re-frame’s effects and replicant’s commands represent side-effectful computations as data, as the abstract atomic operations in our eDSL do.

The Free (Free-er) monad algebraic structure was brought to Clojure before by directly translating the Monad operations to Clojure records (mpivaa/free-monad, eunmin/free-monad, active-group/active-clojure); in contrast, while the algebraic structure guided the construction of our eDSL and interpreter, the resulting eDSL does not refer to any Monad operations; it’s just Hiccup. Our approach arguably results in a more idiomatic Clojure code.

The idea of implementing business logic as functions returning data descriptions of side-effectful computations is very similar to the concept of command handlers from the Event sourcing (ES) pattern; indeed, in the same way as a function in our eDSL receives an output from an operation and returns an eDSL expression that, when evaluated, triggers some side effects, a command handler in ES receives an output from a query and returns a series of events, each of them triggering a side effect when handled.

Contact us

Let's build the software
your company needs

We design and deliver intelligent, tailored software that helps your company operate smarter, move faster, and grow with confidence.