Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine DDD mapping into Clojure #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 15 additions & 19 deletions src/clj_ddd_example.clj
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@
must perform the necessary side-effects the use case demand, such as in our
case updating our persistent state using the repository to update our
datastore about the transfer of money that occurred."
(:require [clj-ddd-example.repository :as repository]
[clj-ddd-example.domain-model :as dm]
[clj-ddd-example.domain-services :as ds]))
(:require [clj-ddd-example.domain-services :as ds]
[clj-ddd-example.repository :as repository]
[clojure.spec.alpha :as s]))

(s/check-asserts true)

(defn transfer-money
"Our first use case, transfer-money, can be used to transfer money from one
Expand All @@ -54,17 +55,12 @@
(try
(let [from-account (repository/get-account from)
to-account (repository/get-account to)
domain-amount (dm/make-amount amount currency)
transfered-money (ds/transfer-money transfer-number from-account to-account domain-amount)]
(repository/commit-transfered-money-event transfered-money)
transfer-money (ds/transfer-money transfer-number from-account to-account {:amount/currency currency
:amount/value amount})]
(repository/commit-transfer-money transfer-money)
{:status :done
:transfered [(-> domain-amount :value) (-> domain-amount :currency)]
:debited-account (-> transfered-money :debited-account :number)
:debited-account-amount [(-> transfered-money :posted-transfer :transfer :debit :amount :value)
(-> transfered-money :posted-transfer :transfer :debit :amount :currency)]
:credited-account (-> transfered-money :credited-account :number)
:credited-account-amount [(-> transfered-money :posted-transfer :transfer :credit :amount :value)
(-> transfered-money :posted-transfer :transfer :credit :amount :currency)]})
:transfered [amount currency]
:transfered-result transfer-money})
(catch Exception e
{:status :error
:transfer-number transfer-number
Expand All @@ -86,12 +82,12 @@

;; Evaluate this to transfer some money

#_(transfer-money
:transfer-number "ABC12345678"
:from "125746398235"
:to "234512768893"
:amount 200
:currency :usd)
(transfer-money
:transfer-number "ABC12345678"
:from "125746398235"
:to "234512768893"
:amount 200
:currency :usd)

;; Evaluate this to run two thousand 1$ transfers in parallel to test the
;; eventual consistency of our implementation.
Expand Down
180 changes: 30 additions & 150 deletions src/clj_ddd_example/domain_model.clj
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,14 @@
;;;;;;;;;;;;;;
;;; Amount ;;;

(s/def :amount/currency
currency?)
(s/def :amount/currency currency?)

(s/def :amount/value
(s/and number? pos?))

(s/def :amount/amount
(s/keys :req-un [:amount/currency
:amount/value]))

(defn make-amount
[value currency]
(s/assert :amount/amount
{:currency currency
:value value}))
(s/keys :req [:amount/currency
:amount/value]))

;;; Amount ;;;
;;;;;;;;;;;;;;
Expand All @@ -186,22 +179,12 @@
;;;;;;;;;;;;;;;
;;; Balance ;;;

(s/def :balance/value
(s/def :account/balance
number?)

(s/def :balance/currency
(s/def :account/currency
currency?)

(s/def :balance/balance
(s/keys :req-un [:balance/currency
:balance/value]))

(defn make-balance
[value currency]
(s/assert :balance/balance
{:currency currency
:value value}))

;;; Balance ;;;
;;;;;;;;;;;;;;;

Expand All @@ -215,76 +198,19 @@
#(s/gen #{"273648898836" "111234871234" "998877324561"})))

(s/def :account/account
(s/keys :req-un [:account/number
:balance/balance]))

(defn make-account
[account-number balance]
(s/assert :account/account
{:number account-number
:balance balance}))

(s/def :debited-account/event #{:debited-account})

(s/def :debited-account/amount-value :amount/value)

(s/def :account/debited-account
(s/keys :req-un [:debited-account/event
:account/number
:debited-account/amount-value]))

(defn- make-debited-account-event
[account-number amount-value]
(s/assert :account/debited-account
{:event :debited-account
:number account-number
:amount-value amount-value}))

(defn debit-account
"Returns a debited-account domain event describing the valid debit state
change that has happened to the Account, so that it can be applied to our app
state eventually."
[account debit]
(if
(and
(= (-> account :balance :currency) (-> debit :amount :currency))
(= (-> account :number) (-> debit :number))
(>= (- (-> account :balance :value) (-> debit :amount :value)) 0))
(make-debited-account-event (-> account :number) (-> debit :amount :value))
(throw (ex-info "Can't debit account" {:type :illegal-operation
:action :debit-account
:account account
:debit debit}))))

(s/def :credited-account/event #{:credited-account})

(s/def :credited-account/amount-value :amount/value)

(s/def :account/credited-account
(s/keys :req-un [:credited-account/event
:account/number
:credited-account/amount-value]))

(defn- make-credited-account-event
[account-number amount-value]
(s/assert :account/credited-account
{:event :credited-account
:number account-number
:amount-value amount-value}))

(defn credit-account
"Returns a credited-account domain event describing the valid credit state
change that has happened to the Account, so that it can be applied to our app
state eventually."
[account credit]
(if
(and
(= (-> account :balance :currency) (-> credit :amount :currency))
(= (-> account :number) (-> credit :number)))
(make-credited-account-event (-> account :number) (-> credit :amount :value))
(throw (ex-info "Can't credit account" {:type :illegal-operation
:account account
:credit credit}))))
(s/keys :req [:account/number
:account/currency
:account/balance]))

(s/def :account/debit
(s/keys :req [:account/number
:amount/value
:amount/currency]))

(s/def :account/credit
(s/keys :req [:account/number
:amount/value
:amount/currency]))

;;; Account ;;;
;;;;;;;;;;;;;;;
Expand All @@ -293,15 +219,6 @@
;;;;;;;;;;;;;
;;; Debit ;;;

(s/def :debit/debit
(s/keys :req-un [:account/number
:amount/amount]))

(defn make-debit
[account-number amount]
(s/assert :debit/debit
{:number account-number
:amount amount}))

;;; Debit ;;;
;;;;;;;;;;;;;
Expand All @@ -310,16 +227,6 @@
;;;;;;;;;;;;;;
;;; Credit ;;;

(s/def :credit/credit
(s/keys :req-un [:account/number
:amount/amount]))

(defn make-credit
[account-number amount]
(s/assert :credit/credit
{:number account-number
:amount amount}))

;;; Credit ;;;
;;;;;;;;;;;;;;

Expand All @@ -338,44 +245,17 @@
(s/def :transfer/creation-date
inst?)

(s/def :transfer/transfer
(s/and
(s/keys :req-un [:transfer/id
:transfer/number
:debit/debit
:credit/credit
:transfer/creation-date])
(fn[{:keys [debit credit]}]
(and (= (:amount debit) (:amount credit))
(not= (:number debit) (:number credit))))))

(defn make-transfer
[transfer-number debit credit]
(s/assert :transfer/transfer
{:id (random-uuid)
:number transfer-number
:debit debit
:credit credit
:creation-date (java.util.Date.)}))

(s/def :posted-transfer/event #{:posted-transfer})

(s/def :transfer/posted-transfer
(s/keys :req-un [:posted-transfer/event
:transfer/transfer]))

(defn- make-posted-transfer-event
[transfer-number debit credit]
(s/assert :transfer/posted-transfer
{:event :posted-transfer
:transfer (make-transfer transfer-number debit credit)}))

(defn post-transfer
"Returns a posted-transfer domain event describing the valid posted state
change that has happened to the Transfer, so that it can be applied to our
app state eventually."
[transfer-number debit credit]
(make-posted-transfer-event transfer-number debit credit))

;;; Transfer ;;;
;;;;;;;;;;;;;;;;

(s/def :transfer/transfer-money
(s/and
(s/keys :req [:account/debit
:account/credit
:transfer/id
:transfer/number
:transfer/creation-date])
(fn [{debit :account/debit
credit :account/credit}]
(and (= (select-keys debit [:amount/currency :amount/value]) (select-keys credit [:amount/currency :amount/value]))
(not= (:account/number debit) (:account/number credit))))))
84 changes: 61 additions & 23 deletions src/clj_ddd_example/domain_services.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,40 @@

Domain services can make use of other domain services as well as the domain
model."
(:require [clojure.spec.alpha :as s]
[clj-ddd-example.domain-model :as dm]))
(:require [clojure.spec.alpha :as s]))

(defn- debit-account
"Returns a debit-account domain event describing the valid debit state
change that has happened to the Account, so that it can be applied to our app
state eventually."
[account amount]
(s/assert :account/account account)
(s/assert :amount/amount amount)
(if (and
(= (:account/currency account) (:amount/currency amount))
(>= (- (:account/balance account) (:amount/value amount)) 0))
{:account/number (:account/number account)
:amount/value (:amount/value amount)
:amount/currency (:amount/currency amount)}
(throw (ex-info "Can't debit account" {:type :illegal-operation
:action :debit-account
:account account
:amount amount}))))

(s/def :transfer-money/transfered-money
(s/keys :req-un [:account/debited-account
:account/credited-account
:transfer/posted-transfer]))

(defn- make-transfered-money-event
[debited-account credited-account posted-transfer]
(s/assert :transfer-money/transfered-money
{:debited-account debited-account
:credited-account credited-account
:posted-transfer posted-transfer}))
(defn- credit-account
"Returns a credit-account domain event describing the valid credit state
change that has happened to the Account, so that it can be applied to our app
state eventually."
[account amount]
(s/assert :account/account account)
(s/assert :amount/amount amount)
(if (= (:account/currency account) (:amount/currency amount))
{:account/number (:account/number account)
:amount/value (:amount/value amount)
:amount/currency (:amount/currency amount)}
(throw (ex-info "Can't credit account" {:type :illegal-operation
:account account
:amount amount}))))

(defn transfer-money
"Returns if money can be transferred from one account to another for some
Expand All @@ -73,17 +92,20 @@
transferring money. Otherwise throws an exception about the transfer not being
possible."
[transfer-number from-account to-account amount]
(s/assert :account/account from-account)
(s/assert :account/account to-account)
(s/assert :amount/amount amount)
(try
(let [debit (dm/make-debit (:number from-account) amount)
credit (dm/make-credit (:number to-account) amount)
debited-account (dm/debit-account from-account debit)
credited-account (dm/credit-account to-account credit)
posted-transfer (dm/post-transfer transfer-number
debit
credit)]
(make-transfered-money-event debited-account
credited-account
posted-transfer))
(let [debit-account (debit-account from-account amount)
credit-account (credit-account to-account amount)]
;; Returns a posted-transfer domain event describing the valid posted state
;; change that has happened to the Transfer, so that it can be applied to our
;; app state eventually.
{:transfer/id (random-uuid)
:transfer/number transfer-number
:account/debit debit-account
:account/credit credit-account
:transfer/creation-date (java.util.Date.)})
(catch Exception e
(throw (ex-info "Money cannot be transferred."
{:type :illegal-operation
Expand All @@ -92,3 +114,19 @@
:to-account to-account
:amount amount}
e)))))

(defn apply-debit-account
"Returns an updated account of the given account with the debit described
by debit-account-event applied to it."
[account debit-account]
(s/assert :account/account account)
(s/assert :account/debit debit-account)
(update account :account/balance - (:amount/value debit-account)))

(defn apply-credit-account
"Returns an updated account of the given account with the credit described
by credit-account-event applied to it."
[account credit-account]
(s/assert :account/account account)
(s/assert :account/credit credit-account)
(update account :account/balance + (:amount/value credit-account)))
Loading