Braid, the new type of chat client that we’ve been building uses the Datomic database for storage. We originally started with Datomic because it seemed like a pretty neat piece of software and fit well with our Clojure-all-the-way-down setup. After having used it for around a year, I recently did a big refactor of our database layer and how we interact with Datomic.
What We Did
Originally, we used Datomic in the way we usually use traditional databases – have a
db namespace, which has a bunch of functions to wrap calling db functions.
While this was reasonable when using a SQL database (since the db functions themselves are defined in SQL (using YesQL) and need a bit of wrapping), it seemed like we were missing out on some of the advantages of Datomic.
transact function takes vectors of normal Clojure data, there seemed to be an opportunity to coalesce how our code communicated with the database.
My goal was to refactor things so that instead of all the database functions directly calling
transact, they would return transaction data, that would be passed into one wrapper function.
A trivial example of the transformation:
run-txns! function, for now, is just a simple wrapper over
datomic.api/transact, supplying the connection argument.
Using this style, we can now do things like logically group database alterations together in one transaction (by
concat-ing the various
*-txn functions) and we now have one place to validate things.
However, there are a few functions that this simple refactor is unable to handle; namely, functions that need to do something with the result of the transaction.
In our codebase, the main offenders are our various
create-* functions, which insert a new entity and return said entity, using the
create-entity! helper function:
The solution I came up with was using Clojure’s metadata facilities to indicate that a particular transaction wishes to return something.
run-txns! can gather all said functions and return the result of giving all of them the created database.
create-entity! now becomes the following:
after-fn parameter is there because the various
create-*! functions want to call a
db->* function to change the returned entity into a map in the format Braid expects to use)
To make this work,
run-txns! now looks like this:
The final thing I wanted to add was having
run-txns! do some validation, taking advantage of the fact that there is now only one place where
datomic.api/transact is called.
Similarly to how I handled return values of transactions, my idea was to have a “check” function in the metadata of a transaction.
run-txns! could take advantage of a neat Datomic function,
with, which gives you a database that would be what happened if you evaluated a transaction, without actually running said transaction.
The implementation now looks like this:
And we can now write transactions that validate the results as follows
Why We Did It
So, what did we gain from all this?
For one, our transaction functions are now all declarative, which I think fits better with the philosophy of using immutable data structures.
Instead of all our database functions performing arbitrary state changes, we isolate state changes to one function,
Also, because of this isolation, we have one good spot in which to perform validation.
The biggest reason though, was that it was fun!