https://github.com/EducatedAlmost/demesne
Read the blog post: https://blog.almost.education/posts/demesne
Demesne is a simple example of CQRS, event sourcing, and domain-driven design in Clojure. It mimics Greg Young’s example application m-r — in just one third the lines of code (217 vs 674).
It imagines a warehouse of named item lines; which can be searched for, have instances checked in and out, be deactivated and reactivated, and be renamed. It runs as an HTTP server with endpoints that can easily be called with JSON content when necessary.
Search for an item:
GET http://localhost:8080/search/3
Content-Type: application/json
{}
Create an item:
GET http://localhost:8080/create/3
Content-Type: application/json
{"name": "Bar"}
Deactivate an item:
GET http://localhost:8080/deactivate/3
Content-Type: application/json
{}
Reactivate an item:
GET http://localhost:8080/reactivate/3
Content-Type: application/json
{}
Check in instances of an item:
GET http://localhost:8080/check-in/3
Content-Type: application/json
{"amount": 45}
Check out instances of an item:
GET http://localhost:8080/check-out/3
Content-Type: application/json
{"amount": 25}
Rename an item:
GET http://localhost:8080/rename/3
Content-Type: application/json
{"name": "Foo"}
The only datastructures are the command, the event, and the aggregate — the first two of which are very simple and static — everything else is functions.
Let’s add a reactivate
function.
- in
server.clj
we add a route,/reactivate/:id
and (optionally) we add a helper functioncommand/reactivate
to construct the command. - we add a handler to
handler/handle
for:ae.demesne.command.type/reactivate
. - we add a behaviour to the aggregate,
item/reactivate
, that creates areactivated
event. - we add a application function for that event,
apply-event :ae.demesne.event.type/item-reactivated
.
- add an endpoint for user to access
- convert the user’s action into an internal representation of it
- link the command to an aggregate behaviour
- raise events for the aggregate behaviour
In this scenario, steps 3 and 4 might seem useless as there is a very simple journey from command/reactivate
→ item/reactivate
→ event/reactivated
. But in more complex domains, each of these steps becomes necessary. A command can call multiple aggregate behaviours, each of which can raise multiple events, creating a tree of consequences.
- command
- external action or instruction
- behaviour
- the action the aggregate takes in response to external action
- event
- the change in state to the aggregate, following the aggregate’s action
The command handler retrieves the aggregate with get-by-id
and saves it, by saving its new events, with save
.
The repository uses a bespoke in-memory event store that stores events in an atom categorised by aggregate id. This makes the function to retrieve events very simple.
(def db (atom {3 [{::item/id 3 ::event/data {...} ::event/version 11}]}))
(defn get-events [id]
(map ::event/data (get @db id)))
::event/version
exists to allow for concurrency checks, should one wish commands to fail if the aggregate has been updated since one last queried its state.
Run the project directly, via :exec-fn
:
$ clojure -X:run-x Hello, Clojure!
Run the project directly, via :main-opts
(-m ae.demesne
):
$ clojure -M:run-m Hello, World!
Run the project’s tests (they’ll fail until you edit them):
$ clojure -T:build test
Run the project’s CI pipeline and build an uberjar (this will fail until you edit the tests to pass):
$ clojure -T:build ci
This will produce an updated pom.xml
file with synchronized
dependencies inside the META-INF
directory inside target/classes
and
the uberjar in target
. You can update the version (and SCM tag)
information in generated pom.xml
by updating build.clj
.
If you don’t want the pom.xml
file in your project, you can remove it.
The ci
task will still generate a minimal pom.xml
as part of the
uber
task, unless you remove version
from build.clj
.
Run that uberjar:
$ java -jar target/demesne-0.1.0-SNAPSHOT.jar
If you remove version
from build.clj
, the uberjar will become
target/demesne-standalone.jar
.
- Add a aggregate behaviour that involves two events.
- Add specs.