10000 GitHub - EducatedAlmost/demesne: An example of domain-driven design, CQRS, and event sourcing, in Clojure.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

EducatedAlmost/demesne

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
8000
 
 
 
 
 
 

Repository files navigation

æ/Demesne

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.

API

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 code

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.

Adding new functionality

Let’s add a reactivate function.

  1. in server.clj we add a route, /reactivate/:id and (optionally) we add a helper function command/reactivate to construct the command.
  2. we add a handler to handler/handle for :ae.demesne.command.type/reactivate.
  3. we add a behaviour to the aggregate, item/reactivate, that creates a reactivated event.
  4. we add a application function for that event, apply-event :ae.demesne.event.type/item-reactivated.

Why?

  1. add an endpoint for user to access
  2. convert the user’s action into an internal representation of it
  3. link the command to an aggregate behaviour
  4. 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/reactivateevent/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

Repository and event store

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.

Usage

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.

Next steps

  • Add a aggregate behaviour that involves two events.
  • Add specs.

About

An example of domain-driven design, CQRS, and event sourcing, in Clojure.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages 33C9

No packages published
0