Clojure Guide — Datomic: Immutable Database Design
In this tutorial, you will learn about Clojure Guide. We cover key concepts, practical examples, and best practices to help you master this topic.
Datomic is a distributed database designed by Clojure's creator Rich Hickey that embraces immutability at its core, treating data as a growing set of facts over time rather than mutable rows that can be updated or deleted.
What You'll Learn
- Datomic's immutable data model
- Datoms, entities, and attributes
- Datalog query language
- Time travel and as-of queries
- Architecture: Transactor, Peer, and Storage
Why It Matters
Datomic's immutable model eliminates entire categories of database problems: no migration scripts (schema is additive), full audit trail (every fact is retained), and temporal queries (query data as it existed at any point). Durga Antivirus Pro uses similar immutable patterns for audit logging.
Real-World Use
Datomic is used by financial systems requiring full audit trails, healthcare for patient history, and analytics platforms that need point-in-time queries.
flowchart LR
A["Datomic"] --> B["Data Model"]
B --> C["Schema"]
C --> D["Datalog"]
D --> E["Time Travel"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Datomic Data Model
Data in Datomic is stored as datoms — atomic facts:
;; A datom: [entity-id attribute value transaction-id]
[123 :person/name "Alice" 1001]
[123 :person/age 30 1001]
[123 :person/email "alice@example.com" 1002]
Each datom is an immutable fact. You never overwrite — you add new facts. The database is the set of all datoms ever asserted.
Defining a Schema
;; schema.edn
[{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/doc "A person's full name"}
{:db/ident :person/email
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :person/friends
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}]
Transacting Data
(require '[datomic.api :as d])
;; Connect to database
(let [conn (d/connect "datomic:mem://hello")]
;; Transact schema
(d/transact conn [{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}])
;; Transact data
(d/transact conn [{:person/name "Alice"
:person/email "alice@example.com"}]))
Querying with Datalog
;; Find all person names
(d/q '[:find ?name
:where
[?e :person/name ?name]]
(d/db conn))
;; Find with multiple conditions
(d/q '[:find ?name ?email
:where
[?e :person/name ?name]
[?e :person/email ?email]]
(d/db conn))
Parameterized Queries
;; Query with parameters
(d/q '[:find ?name
:in $ ?min-age
:where
[?e :person/age ?age]
[(>= ?age ?min-age)]
[?e :person/name ?name]]
(d/db conn) 25)
Time Travel
;; Query as of a specific time
(let [db-old (d/as-of (d/db conn) #inst "2024-01-01")]
(d/q '[:find ?name :where [?e :person/name ?name]] db-old))
;; Query at a specific transaction
(d/q '[:find ?name :where [?e :person/name ?name]]
(d/as-of (d/db conn) 1001))
;; History of an entity
(d/q '[:find ?attr ?value ?tx
:where
[?e :person/name "Alice"]
[?e ?attr ?value ?tx]]
(d/history (d/db conn)))
Pull API
;; Pull entity attributes
(d/pull (d/db conn) '[*] 123)
;; Pull with nested refs
(d/pull (d/db conn) '[:person/name :person/email {:person/friends [:person/name]}]
123)
Common Mistakes
1. Thinking of Datomic like SQL
Datomic is not relational in the SQL sense. You model with attributes on entities, not normalized tables with foreign keys.
2. Forgetting cardinality-many
Attributes are cardinality-one by default. If an entity can have multiple values, set :db/cardinality :db.cardinality/many.
3. Not using unique identities
Use :db/unique :db.unique/identity for natural keys (like email) to enable upsert semantics.
4. Ignoring Transaction time
Every query runs against a point-in-time database value. Always consider which database value you are querying.
5. Over-normalizing data
Datomic's entity-attribute model handles denormalized data well. Don't split data into many small entities unless necessary.
Practice Questions
1. What is a datom?
A datom is a single atomic fact: [entity-id attribute value transaction-id]. Datoms are immutable and additive.
2. How does Datomic differ from SQL databases? Datomic uses an entity-attribute-value model, stores all historical data immutably, and separates reads from writes through the peer/transactor architecture.
3. What is the as-of query? An as-of query returns the database state as it existed at a specific point in time, enabling time travel queries.
Challenge: Design a Datomic schema for a blog with posts, authors, and comments that supports full audit history.
FAQ
{{< faq question="Is Datomic open source?" >}} Datomic has a free tier and a commercial license. The Pro version is free for individual developers. The source code is not open source but is available under a license. {{< /faq >}}
{{< faq question="How does Datomic handle scalability?" >}} Datomic separates reads (peers) from writes (transactor). Peers are stateless and cache data locally. The transactor serializes writes. This architecture scales reads horizontally. {{< /faq >}}
{{< faq question="Can Datomic delete data?" >}} You can retract datoms, which marks them as no longer true, but the historical data remains. This enables full audit trails while supporting current-state queries. {{< /faq >}}
{{< faq question="What storage engines does Datomic support?" >}} Datomic supports DynamoDB, SQL databases (PostgreSQL, MySQL), Cassandra, and an in-memory storage option for development. {{< /faq >}}
{{< faq question="Is Datalog hard to learn?" >}} Datalog is simpler than SQL for many use cases. It uses logic programming concepts that map naturally to Clojure's data-oriented philosophy. {{< /faq >}}
Mini Project
Create a simple contact management system with Datomic:
(def schema
[{:db/ident :contact/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :contact/email
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :contact/phone
:db/valueType :db.type/string
:db/cardinality :db.cardinality/many}])
(def conn (d/connect "datomic:mem://contacts"))
(d/transact conn schema)
;; Add contacts
(d/transact conn [{:contact/name "Alice" :contact/email "alice@example.com"}])
;; Query all contacts
(d/q '[:find ?name ?email
:where
[?c :contact/name ?name]
[?c :contact/email ?email]]
(d/db conn))
What's Next
Now that you understand Datomic, explore web development with Ring and Compojure.
| Topic | Description | Link |
|---|---|---|
| Clojure Web Development | Building web applications | {{< ref "20-web-development" >}} |
| Clojure Ring | HTTP server library | {{< ref "21-ring" >}} |
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro