Domain-Driven Design in Clojure

This article is a work in progress.

Domain-Driven Design is a fascinating approach to tackle the complexity in software design. In particular, the idea of explicitly modelling one’s domain and cultivating an ubiquitous language in the team resonate with me.

Unfortunately, most DDD resources, in particular the books by Evans and Vernon, are quite focused on object-oriented programming, and therefore not appealing to Clojure programmers. In this article, I discuss how to translate DDD concepts into a Clojure mindset.

Before we dive into the DDD building blocks, let us first look at how Clojure designs programs.

The Epochal Time Model

In his talk Are we there yet?, Rich Hickey discusses his view on how OOP went wrong, and describes Clojure’s approach to identity and state as the Epochal Time Model.

Epochal Time Model

Instead of mutating objects, Clojure prefers to implement logic with pure functions operating on immutable data structures. An identity is a succession of states: Immutable values at a point in time. Rather than interacting with constantly changing objects, observers of an identity receive a snapshot of the identity in form of an immutable value.

Clojure offers several tools to represent identities in a thread-safe manner, such as Refs, Agents, and Atoms.

To do: Bigger picture: This model also applies to distributed systems. Here, identities need to be coordinated in a database.

Building Blocks

In the second part of his DDD Reference, Evans summarizes the building blocks for model-driven design. Let’s see how they translate to Clojure!

Layered Architecture

The domain model implements all business logic and should be isolated from infrastructure, user interface, and application logic.

The idea of separating concerns into "layers", and to manage dependencies between them, is pretty popular and advocated by different architectural patterns such as Hexagonal Architecture or Clean Architecture. While the details vary, common layers include:

Layer Description

Infrastructure

Adapters to third-party libraries or frameworks, such as databases, external services, and so on.

Presentation

Handling of outside requests e.g. via UI, HTTP, command-line interfaces, or similar. Displaying or providing information to the user or client system.

Application

Use cases — how the application interacts with the domain model. Handles concerns such as transactions and security.

Domain

Business logic implemented in the domain model, independent of deployment considerations.

Since this concept is independent of the underlying programming paradigm, it can be applied to Clojure programs directly, by assigning namespaces to layers and agreeing on allowed dependencies. Conventionally, namespaces can represent the layer they belong to using prefixes:

  • <project>.infra

  • <project>.web (or <project>.ui, etc.)

  • <project>.application (or <project>.app, <project>.api)

  • <project>.domain

We can even enforce the architecture by writing tests that use the Namespaces API or the dependency graph provided by tools.namespace to verify that namespaces only depend on allowed namespaces.

Finally we can "physically" separate layers by splitting our code into separate projects (deps.edn) or modules (Leiningen) with dedicated classpaths.

Value Objects

Immutable objects that are identified by their attributes only.

This is a no-brainer for Clojure programmers because Clojure embraces immutable values. There is a wide range of immutable primitives, from numbers and strings to more specialized types such as UUIDs, dates and times.

Custom domain values will typically be represented by built-in Clojure types such as maps, vectors or keywords, rather than custom types.

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.
— Alan Peris

It makes sense to define specs describing the type and putting it in a namespace along pure functions operating on the values. This keeps related concepts close together.

(ns project.domain.money
  (:require [clojure.spec.alpha :as s]))

(s/def ::amount int?)
(s/def ::currency keyword?)
(s/def ::money (s/keys :req [::amount ::currency]))

(defn money
  "Creates a money value."
  [amount currency]
  {::amount amount
   ::currency currency})

(defn m+
  "Adds to money values."
  [a b]
  (if (= (::currency a) (::currency b)
    (update a ::amount + (::amount b)
    (throw (ex-info "Currency mismatch" {:lhs a, :rhs b}))))))

Consider using generative testing for custom values.

Entities

Entities are classical "reference objects" whose identity is important for the domain, but whose attributes change over time. When persisted in a database, these have a unique ID representing the identity. They encapsulate data and expose behavior.

As described in The Epochal Time Model, Clojure prefers to separate identity and state. For the state, it is most natural to represent entities as persistent maps, essentially treating them the same as values:

(ns project.domain.bank-account
  (:require [clojure.spec.alpha :as s]
            [project.domain.money :as money]))

(s/def ::account-number int?)
(s/def ::balance ::money/money)

(s/def deposit [account amount]
  (update account ::balance money/m+ amount))

In other words, entities are values, with the additional constraint that they are maps and contain some form of identifying attribute, like ::account-number in the example above.

We will discuss ways of how to represent changing states with stable identity in Aggregates.

Domain Events

To do…​

Services

To do…​

Modules

To do…​

Aggregates

Aggregates define units of consistency, by clustering entities and value objects that require strong consistency guarantees. When using a database, aggregates are persisted as one unit, in one transaction, to guarantee consistency.

Aggregates are represented by one entity in the cluster, named the root. As such, we will represent their state the same as other entities in Clojure: as maps. Since aggregates form a unit, their map representation will include all nested entities and values. All external access to the values in an aggregate go through the aggregate root.

Changing an entity entails changing the whole aggregate. Functions operating on aggregate values can delegate to entity-changing functions:

(ns project.domain.aggregate
  (:require [clojure.spec.alpha :as s]
            [project.domain.entity :as ent]))

(s/def ::id string?)
(s/def ::children (s/coll-of ::ent/entity))
(s/def ::aggregate (s/keys :req [::id ::children]))

(defn update-child [aggregate index]
  (update-in aggregate [::children index] ent/update-entity))

A simple single-process application without data persistence could use Clojure’s state primitives to represent an aggregate’s identity, such as atoms or refs.

Usually aggregates will be stored in a database, and accessed using Repositories.

Repositories

To do…​

Factories

To do…​

Application

To do…​

References