BandSquare
Tour of a Clojure + Datomic app
Valentin Waeselynck
Mail: val.vvalval@gmail.com
Twitter: @val_waeselynck
Github: vvvvalvalval
About BandSquare
BandSquare’s Stack
BandSquare’s Stack - History
Backoffice (static site)
bandsquare.com
(static site)
Backend server
(Clojure - Java)
Analytics
Mandrill
(email delivery)
Stripe / Paypal
(online payment)
Digitick
(ticketing)
(AMQP - CloudAMQP)
AWS DynamoDB
Datomic
RedisLabs
KV Stores / Cache
AWS Elastic Beanstalk
Netlify
Netlify
AWS S3
Static storage
Scheduler
(SetCronJob)
Social Networks
URL Shorteners
etc.
BandSquare’s Stack - Why ?
BandSquare’s Stack - Why ?
Datomic
Datomic: DB values and connections
2 notions:
(agent #{[42 :movie/title "Gladiator"]
[43 :movie/title "Babel"]
[101 :actor/name "Russell Crowe"]
[42 :movie/starring 101]
...})
Datomic: architecture
Datomic: the database is effectively local
Datomic querying: Datalog and Entities
;; Entities: find the director name of Gladiator
(def gladiator
(d/entity db [:movie/title "Gladiator"]))
(def director (get gladiator :movie/director))
(get director :director/name)
=> "Ridley Scott"
;; or more concisely
(-> gladiator :movie/director :director/name)
=> "Ridley Scott"
;; Entities have a Map-like interface
(select-keys gladiator
[:db/id :movie/title :movie/director])
=> {:db/id 42
:movie/title "Gladiator"
;; that's another Entity
:movie/director {:db/id 23}}
;; Datalog: find directors of Russell Crowe
(d/q
'[:find [?director ...]
:in $ ?actor-name :where
[?actor :actor/name ?actor-name]
[?movie :movie/starring ?actor]
[?movie :movie/director ?director]]
db "Russell Crowe")
=> #{23}
Datomic querying: Datalog and Entities
Datalog:
Entities:
Datomic querying: Datalog and Entities
42
:movie/title
"Gladiator"
23
:movie/director
:director/name
"Ridley Scott"
101
:actor/name
"Russell Crowe"
:movie/starring
Datomic superpowers
Architecture
Design
Design goals:
Constraints:
Architecture: contexts
(def ^:dynamic conn ...)
(def ^:dynamic mail-sender ...)
(defn send-email! [customer data]
(mail/send-mail! mail-sender
(:customer/email customer) data)
(d/transact conn
[{:mail/id ...
:mail/to ...}]))
(defn send-email!
[{:as ctx :keys [conn mail-sender]}
customer data]
(mail/send-mail! mail-sender
(:customer/email customer) data)
(d/transact conn
[{:mail/id ...
:mail/to ...}]))
BAD
GOOD
Architecture: contexts
{:conn ;; Datomic Connection
#datomock.core.MockConnection{:a_db #<Atom@1a588bc3: datomic.db.Db@413e79b5>,
:a_txq #<Atom@9ef48b4: nil>}
:db ;; Datomic current database value
datomic.db.Db@413e79b5
:mail-sender ;; mail sending service
#bs.toolkit.mail.MockMailSender{:badRecipients #{"bad.email@gmail.com"},
:latency 10},
:stripe ;; Stripe (payment) client service
#bs.services.stripe.MockStripeClient{:latency 10}
:local-cache ;; in-memory caching service
#bs.toolkit.stores.AtomTtlCache{:a #<Atom@473d9abb: {}>},
:now ;; current time
#inst "2016-06-26T13:31:43.936-00:00"}
Anatomy of a context:
Architecture: contexts
Services declared via protocols / interfaces
(defprotocol MailSender
(send-template!* [this data])
(send-text!* [this data])
)
(defprotocol StripeClient
(call [this endpoint req]))
Architecture: the api multimethod
(defmulti api (fn [ctx proc-name params data]
proc-name))
Architecture: the api multimethod
;; registering an endpoint (implements the api multimethod)
(api/endpoint ::add-concert-to-campaign
{:middlewares [mid_user-may-access-campaign] ;; authorization middleware
:data-schema {:id sc/Str, :venueName sc/Str}} ;; Prismatic Schema
(fn handler
[{:as ctx, :keys [conn db]} ;; context - ‘Dependency Injection’
{:as params, :keys [campaignId]} ;; params: resource identification
{:as concert-data, :keys [id venueName]}] ;; data: what to write
(let [campaign (find-campaign-by-id db campaignId) ;; finder fn: (db,data) -> Entity
tx (tx_add-concert-to-campaign campaign concert-data)
{:keys [db-after]} @(d/transact conn tx)
concert (find-concert-by-id db-after id)]
(cl_concert concert) ;; clientizer fn: Entity -> serializable data structure
)))
;; ... called by HTTP route
(POST "/campaigns/:campaignId/concerts" ::concerts/add-concert-to-campaign)
Architecture: data flow
HTTP request
API request
Entities
conn
Data response
HTTP response
Context injection
Parsing
HTTP Routing
Handling
Finding Entities
Clientization
Serialization
Writes
Side effects
Business logic
API
Business logic: Entities in, Entities out
(require '[datomic.api :as d])
(defn past-concerts-of-campaign
"Given a campaign, find those concerts of the campaign which have already happened."
[now campaign]
(let [db (d/entity-db campaign)]
(->> (d/q '[:find [?concert ...] :in $ ?campaign ?now :where
[?concert :concert/campaign ?campaign]
[?concert :concert/time ?time]
[(< ?time ?now)]]
db (:db/id campaign) now)
(map #(d/entity db %))
)))
Business logic: Entities in, Entities out
(require '[datomic.api :as d])
(defn past-concerts-of-campaign
"Given a campaign, find those concerts of the campaign which have already happened."
[now campaign]
(->> campaign
:concert/_campaign
(filter (fn [concert]
(-> concert :concert/time (time/before? now)))))
)
Architecture: high-level overview
Contexts: the Universe as an Input
Database connection
Payment service
Email service
Payments
Manager
Signup
Manager
Application
External services (universe)
Application components
Application
Traditional, DI-style system structure
Request / Event
Response
Contexts: the Universe as an input
Request / Event
Response
Universe
Application
Request / Event
Universe
(a.k.a context)
side-effects
(cf Hexagonal Architecture)
Forking the universe
Questions?
Valentin Waeselynck
Mail: val.vvalval@gmail.com
Twitter: @val_waeselynck
Github: vvvvalvalval