A blog series about learning Datomic with examples
Are you looking for a way to get started learning Datomic, or are you picking Datomic up again after some time away from it? If so, this tutorial is for you. By building a straightforward TODO list app with Clojure and Datomic, we will identify the mental model of Datomic and demonstrate how to use its theoretical insights in a practical context. Your journey through the tutorial will equip you with the skills to continue exploring Datomic on your own. You will learn key characteristics of what makes Datomic special:
As we go along, we will primarily reference docs.datomic.com and list other resources for further reading and learning and the end of each part of the tutorial.
This tutorial is designed for Clojure and Datomic beginners or someone picking up Datomic again. If you are totally new to Clojure, you should seek out some more introductory content before coming back here.
This guide aims to provide you with the skills and knowledge to get Datomic’s mental model and gain confidence to continue explore Datomic by yourself.
Objectives of the Tutorial
By the end of this tutorial, you will:
This structured approach ensures you gain both theoretical and practical insights, making it easier to gain confidence to continue explore Datomic.
In a new terminal, verify that the requirements are installed properly by running the following commands.
clojure --version
;; Output
Clojure CLI version 1.12.0.1479 ;; the version might be different in your machine.
It's recommended to install the latest stable version of Clojure.
java --version
;; Output
java 21.0.4 2024-07-16 LTS
It should work with Java 11+.
A Datomic database is a single universal relation of facts called datoms. Datoms can be either assertions indicating that something is known to be true at a point in time or retractions indicating that something is no longer known to be true. A fact is something that occurred, time is part of the definition thus we can reason about the database with the time at hand. "At this moment of time my database is this value", the queries I make will return the same result until I pass a different value of the database. Comparing it to a server oriented database, things could have changed between query one and query two, because we are asking questions in terms of a place, depends on what's in the place when you ask.
Datomic is a database that is fundamentally different, as we go over the tutorial we'll exemplify what is stated above. The docs website contains a very complete explanation on the architecture of Datomic, the tutorial will only explain some concepts briefly and it's encouraged to complement them reading the documentation.
Data is modeled as entities, attributes, values, transaction, and op information all in a single tuple [entity, attribute, value, transaction op]
. In Datomic this is called a “datom” (An atomic fact in a database).
We’ll refer to this tuple as [e a v t op]
.
Given the following map
(def db-example
[{:list/name "life"
:list/items [{:item/text "travel"
:item/status :item.status/:todo}
{:item/text "buy coffee"
:item/status :item.status/:todo}]}
{:list/name "learn"
:list/items [{:item/text "clojure"
:item/status :item.status/:todo}
{:item/text "datomic"
:item/status :item.status/:todo}]}])
we can represent it in a Datomic database like this;
values in the image are simplified
Datomic is a single table that accumulates datoms, the "e" position is the entity identifier, all of the datoms that have the 45418 in the "e" relate to the same entity, we call this number eid. The "a" is self explanatory, refers to the attribute we are talking about. The "v" has the value of such attribute. The "t" is the transaction, it tells when the datom was persited and it's possible that many datoms share the same "t", in the example we se the number 34321 in 6 places, that tells us that those 6 datoms were transacted at the same moment. Lastly, "op" is a boolean that tells if that datom remains true, we will talk more about this when introducing retraction, to briefly explain it, when the "op" is false means that the "v" is no longer a reality.
Datomic makes first-class relationships
The arrows ilustrate that number 45422 points to the "travel" datom and that 45427 points to the "datomic" datom. Datomic provides these references for users to make the relationships.
Attribute "a" can be defined as cardinality one or cardinality many. "One" means only one datom for a given eid can be true or absence of value. "Many" means that multiple datoms for a given eid can hold the true "op", :list/items
is an example of that, we see [45418 :list/items 45422 344317 true]
and [45418 :list/items 45424 344319 true]
. In these :list/items
datoms we have number 45422 and number 45424 in the "v" position, both are part of the "life" list.
When data is transacted to the database Datomic will create a t datom that will contain the time information about when those “datoms” were added. Datomic will place it in the “t” spot of [e a v t]
We want to build an app that will do everything we typically expect of a TODO app (create list items, mark items completed, delete items), plus a couple of powerful features that Datomic makes it trivial to enable them. For example, have you ever wanted add some time filters so that you could ask your app, "How was my life yesterday?" and display the evolution of the statuses of an item, a history log of changes. In Part 1, we'll focus on the foundation that will allow us to add more features as we go.
We'll keep dependencies small.
Choose a place in your file system to create a new project, in this tutorial we’ll use the path when a new terminal is open. We’ll follow this structure.
.
├── deps.edn
└── src
└── todo_db.clj
The tutorial focuses in explaining key concepts about Datomic and exemplifying them with code examples that run in the Clojure REPL. When there is a code block with ;; REPL
comment at the top it means that the code should run the the REPL. If it doesn't have that comment it's just an example to assist an explanation.
The code can be found in datomic-tutorials
Open a new terminal and download datomic-pro
curl https://datomic-pro-downloads.s3.amazonaws.com/1.0.7277/datomic-pro-1.0.7277.zip
unzip the file
unzip datomic-pro-1.0.7277.zip -d .
The transactor is a process with the ability to commit transactions for a given database.
In the same terminal, run the following command to start the transactor:
datomic-pro-1.0.7277/bin/transactor config/samples/dev-transactor-template.properties
;; Output
Launching with Java options -server -Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=50
System started
Now that we have a running transactor we can start working on the code.
Let's do some REPL exploration and get some practice with Datomic essentials:
Add the following to deps.edn
.
{:deps {org.clojure/clojure {:mvn/version "1.12.0"}
com.datomic/peer {:mvn/version "1.0.7277"}
io.pedestal/pedestal.jetty {:mvn/version "0.7.1"}
org.slf4j/slf4j-simple {:mvn/version "2.0.10"}
hiccup/hiccup {:mvn/version "2.0.0-RC3"}}}
For now, we'll focus in the com.datomic/peer
library. The rest will come handy when building the browser UI.
Schemas follow the same rules as transacting any other data, they are also datoms.
Name | Purpose | Required? |
---|---|---|
:db/ident | specifies a unique programmatic name for an entity (normally a schema entity) | Required for schema entities |
:db/cardinality | specifies whether an attribute associates a single value or a set of values | Required |
:db/valueType | specifies the type of value that can be associated with an attribute | Required |
:db/unique | specifies a uniqueness constraint for the values of an attribute | Optional |
To know more, take a look at the full list of attributes available for schemas in the official docs.
Let's define the uses cases and properties of the application and then create a schema that matches the requirements.
The relationship can be, "lists have many items", in other words the cardinality of items in terms of list is "many".
When :db.type/ref
is defined, the reference can be to any other entity in the database, Datomic doesn't restrict to reference only to a specific entity, that's a bussiness domain defined in the application.
;; REPL
(def schema
[{:db/ident :list/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db/doc "List name"}
{:db/ident :list/items
:db/valueType :db.type/ref;; reference
:db/cardinality :db.cardinality/many
:db/doc "List items reference"}
{:db/ident :item/status
:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/one
:db/doc "Item Status"}
{:db/ident :item/text
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Item text"}])
Learn more about defining schema in Datomic here
To transact a schema you need to:
db-uri
;; REPL
(def db-uri "datomic:dev://localhost:4334/todo")
;; REPL
(d/create-database db-uri)
;; REPL
(def conn (d/connect db-uri))
;; REPL
@(d/transact conn schema)
d/transact returns a promise, we use @ to deref it
With the schema transacted we are able to store some lists and items.
Datomic represents transaction data as data structures. This is a significant difference from SQL databases, where requests are submitted as strings. Using data instead of strings makes it easier to build requests programatically. Remember that a datom is [e a v t], when transacting data we need to provide the first 3 and Datomic will create the “t”. Datomic has two forms of structures, list and maps.
This is the structure of a list form [op entity-id attribute value]
(d/transact conn [[:db/add "42" :list/name "life"]])
The map form is a convenient shorthand when making several assertions about the same entity. The map has an optional :db/id key identifying the entity, plus any number of attribute/value pairs. When creating a new entity we can exclude the :db/id and Datomic will generate a new one.
(d/transact conn [{:list/name "life"}])
Now transact a new list with name "life"
;; REPL
@(d/transact conn [{:list/name "life"}])
;; Result
{:db-before datomic.db.Db@c5277e1,
:db-after datomic.db.Db@b415356f,
:tx-data
[#datom[13194139534316 50 #inst "2025-03-25T19:54:47.185-00:00" 13194139534316 true]
#datom[17592186045421 72 "life" 13194139534316 true]],
:tempids {-9223300668110598127 17592186045421}}
The result of a transaction contains;
:db-before
= database value before the transaction:db-after
= database value after the transaction:tx-data
= datoms produced by the transaction:tempids
= tempid resolution, from the string we chose to the actual value in the database.The tx-report enables straight forward comparisons before and after data is transacted. We can use the
:db-before
to make queries to the past. Remember, with Datomic the db's are values and we can pass those values around, queries will return the same result if it's executed to the same db value. Db values don't mutate, you can take your time and process them, there is no need to lock the world.
To showcase a quick example of that, create another list learn
, but this time we are going to save the result in a var and then access to the result values.
;; REPL
(def result @(d/transact conn [{:list/name "learn"}])) ;; new list
(def my-list-q
'[:find ?list
:in $
:where [?list :list/name "learn"]])
(d/q my-list-q (:db-after result)) ;; IMPORTANT line
;; Result
#{[17592186045423]} ;; the result number might be different for you, it's okay.
;; REPL
(d/q my-list-q (:db-before result))
;; Result
#{}
With (def result @(d/transact conn [(new-list "learn")])
, you can run the same query with (:db-after result)
or (:db-before result)
, the first returns the eid of the list in the database, the second is empty because at that point in time the list didn't existed.
This is a simple example of the out-of-the-box support for making queries at different moments of time by leveraging a database as a value rather than a connection.
learn more about Datomic time model
With a list transacted, let's start adding some items. We'll follow the same pattern: create a function new-item
that receives a list-name and the todo string. We are also including the db as we want to get the eid of the list to make the correct relationship.
Add new-item
function
;; REPL
(defn new-item [db list-name item-text]
{:db/id (d/entid db [:list/name list-name])
:list/items [{:db/id (d/tempid :db.part/user)
:item/text item-text
:item/status :item.status/todo}]})
To get the eid of an entity we can use d/entid
function that receives a db and a lookup ref. We pass the eid to tell Datomic that we want to do an assertion over an existing entity, add a new item. Notice that for the :db/id
inside :list/items
we make use of tempids by calling the function d/tempid. This is because we are creating a new nested entity. Datomic requires that a nested map either be referenced by a component attribute or include a unique attribute. This constraint prevents the accidental creation of easily-orphaned entities that have no unique identity or relation to other entities.
;; REPL
@(d/transact conn [(new-item (d/db conn) "life" "travel")])
let's populate the life
list with more data to run some exploration queries.
;; REPL
(->> ["play drums" "scuba dive" "buy coffee"]
(map (partial new-item (d/db conn) "life"))
(d/transact conn))
Add items to the learn
list.
;; REPL
(->> ["clojure" "datomic" "sailing" "cook rissotto"]
(map (partial new-item (d/db conn) "learn"))
(d/transact conn))
An addition indicates that a datom is true at a point in time, and a retraction indicates that a datom is not true at a point in time. When adding a retraction, it is persiting a datom with the "op" as false, that tells Datomic that the fact can be ignore for the current present, unless explicitly asking to include datoms that are false, we will cover more on this on part 4 when talking about filters.
retract the "buy coffee" item from the "life" list.
:db/retract
fn and pass the eid;; REPL
(def eid (ffirst
(d/q '[:find ?item
:in $
:where [?item :item/text "buy coffee"]]
(d/db conn)))
@(d/transact conn [[:db/retract eid :item/text]])
to ilustrate the accumulation of datoms in the table we can see that [45424 :item/text "buy coffee" 34322 false]
is added. It is placed in that position because Datomic maintains four covering indexes that contain ordered sets of datoms. The picture shows EAVT, which is sorted first by "E" then "A" then "V" and finaly "T", that's why we see the descending ordering in the A column.
Datomic uses Datalog as query engine. A query finds values in a database subject to the given constraints, and is specified as edn. Queries are modeled following the same pattern of a datom [e a v t]
, if we understand this structure queries can become very powerful.
Currently we transacted 2 lists and a few items. Let's start with some simple queries to get things going.
The query engine runs locally in the application server, queries execute with a local db value not against a connection. You can hold the value as much time as you want and the result will always be the same. Running queries through a connection doesn't guarantee that you get the same value every time.
Get lists and their names
;; REPL
(d/q '[:find ?list-name
:in $
:where [?list :list/name ?list-name]]
(d/db conn))
;; Result
#{["life"] ["learn"]}
:find
= specifies what you want to be returned from the query. In this case, ?list
is a logic variable that will be bound within the :where
clause.:in
= tells what are the inputs that the query will run against. The $ is the database and the rest are optional logic variables.:where
= tells what datoms to retrieve.Get lists + items
;; REPL
(d/q '[:find ?list-name ?items
:in $
:where [?list :list/name ?list-name]
[?list :list/items ?items]]
(d/db conn))
;; Result
#{["life" 17592186045422] ["life" 17592186045426] ["life" 17592186045425]
["life" 17592186045424] ["learn" 17592186045428] ["learn" 17592186045429]
["learn" 17592186045430] ["learn" 17592186045431]}
A set of vectors repeating the list name? similar to Clojure, at first glance looks different and it's because it is a different way to interact with a database. Let's break it down, first thing we see is the repetition of the list name, e.g ["life" 17592186045425]
and the same case for "learn". Our schema is defined as :db.cardinality/many
on the :list/items
attribute, in other words we are allowing many items being referenced by :list/items
, that makes the result make sense, it's telling us that the list "life" has many items, each one being a reference.
Get lists + items different version
;; REPL
(d/q '[:find ?list-name (vec ?items)
:in $
:where [?list :list/name ?list-name]
[?list :list/items ?items]]
(d/db conn))
;; Result
[["learn" [17592186045428 17592186045429 17592186045430 17592186045431]]
["life" [17592186045422 17592186045426 17592186045425 17592186045424]]]
The difference is (vec ?items)
which is grouping all of the items of a list. Also the items are numbers and that is because we are just pulling the reference number to the entity (eid), if we want to get the attributes of the Item we need to ask that explicitly.
Get lists + items using pull
Entities do not exist on their own, entities are an association of datoms.
In this section we will introduce the expression pull. Pull is a declarative way to make hierarchical (and possibly nested) selections of information about entities. Pull applies a pattern to a collection of entities, building a map for each entity.
;; REPL
(d/q '[:find (pull ?list [:list/name {:list/items [:item/text]}])
:in $
:where [?list :list/name ?list-name]]
(d/db conn))
;; Result
[[#:list{:name "life"
:items [#:item{:text "travel"}
#:item{:text "play drums"}
#:item{:text "scuba dive"}]}
[#:list{:name "learn"
:items [#:item{:text "clojure"}
#:item{:text "datomic"}
#:item{:text "sailing"}
#:item{:text "cook rissotto"}]}]]]
As a simplification, we will order items by their entity eid, which corresponds to transaction order (when only one item is added per transaction.) If different sorts are desired, you can sort the items "in post query", using normal Clojure code, or you could assert additional attributes about list items to facilitate different sorts.
We have:
With some embellishment of the code, we can have this initial src/todo_db.clj
file
(ns todo-db
(:require
[datomic.api :as d]))
(def schema
[{:db/ident :list/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity
:db/doc "List name"}
{:db/ident :list/items
:db/valueType :db.type/ref;; reference
:db/cardinality :db.cardinality/many
:db/doc "List items reference"}
{:db/ident :item/status
:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/one
:db/doc "Item Status"}
{:db/ident :item/text
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "Item text"}])
(def db-uri "datomic:dev://localhost:4334/todo")
;; INFO: Creates the database, if it does not exist returns false
(d/create-database db-uri) ;; it requires a running transactor
;; INFO: to delete a database use `d/delete-database`
(comment (d/delete-database db-uri))
;; Connect to the database
(def conn (d/connect db-uri))
(comment @(d/transact conn schema))
(defn new-list
"receives a `list-name` and returns a new List datom."
[list-name]
{:list/name list-name})
(defn new-item
"recives a `db` a `list-name` and the `item-text` and
returns a map form of datoms to add items to a list."
[db list-name item-text]
{:db/id (d/entid db [:list/name list-name])
:list/items [{:db/id (d/tempid :db.part/user)
:item/text item-text
:item/status :item.status/todo}]})
Published: 2025-04-30
Tagged: datomic-pro tutorial todo-app