Datomic enforces schema at transaction time, mainly through the definitions attached to each attribute.
A Datomic attribute is itself an entity in the database. At minimum, it declares:
{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
The transactor uses that definition to validate every new datom.
1. Attributes must already exist
You cannot normally assert:
[:db/add person-id :person/name "Alice"]
unless :person/name has been installed as a schema attribute.
Unlike a document database, Datomic does not silently create arbitrary fields.
2. Value type is enforced
Given:
{:db/ident :person/age
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one}
this is valid:
[:db/add person-id :person/age 44]
but this transaction is rejected:
[:db/add person-id :person/age "forty-four"]
The value of every datom must match the attribute’s declared :db/valueType.
3. Cardinality is enforced
An attribute declares either:
:db.cardinality/one
or:
:db.cardinality/many
For cardinality-one, an entity can have only one current value for that attribute. Adding another value replaces the previous current value through a retraction plus assertion.
[:db/add person-id :person/name "Alice"]
[:db/add person-id :person/name "Alicia"]
The current database contains only "Alicia", although history retains both facts.
For cardinality-many, Datomic maintains a set:
{:db/ident :person/tags
:db/valueType :db.type/keyword
:db/cardinality :db.cardinality/many}
[:db/add person-id :person/tags :founder]
[:db/add person-id :person/tags :developer]
Both values coexist.
4. References are type-checked as entity references
Relations are represented with attributes whose value type is:
:db.type/ref
For example:
{:db/ident :person/company
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}
This enforces that the value is an entity reference, not an arbitrary string:
[:db/add person-id :person/company company-id]
But Datomic does not inherently enforce that the referenced entity is specifically a “Company.” Datomic has no built-in table/class boundary saying:
:person/company must point only to entities of type Company.
That richer domain constraint needs an entity spec, transaction function, or application-level validation.
5. Uniqueness is enforced globally
You can declare:
{:db/ident :person/email
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
Datomic then prevents two distinct entities from having the same current email.
:db.unique/identity also gives the attribute identity semantics: a transaction referring to the same unique value can resolve or upsert the same entity.
There is also:
:db.unique/value
which enforces uniqueness but does not provide the same upsert identity behavior. Only cardinality-one attributes can be unique.
6. Schema is global per attribute, not per entity type
This is the most important difference from SQL.
In SQL, the schema belongs to a table:
Person
name string
age integer
In Datomic, schema belongs to attributes:
:person/name → string, cardinality one
:person/age → long, cardinality one
Any entity may technically possess any installed attribute:
[:db/add some-order-id :person/name "Strange but valid"]
Datomic will accept this as long as the value satisfies the attribute definition.
So Datomic enforces:
what values an attribute accepts;
how many current values it can have;
whether values must be unique;
whether it is a reference;
certain storage and lifecycle semantics.
It does not automatically enforce:
required fields;
entity classes;
allowed combinations of attributes;
“only Persons may have :person/name”;
“an Order must contain at least one item”;
cross-attribute business rules.
7. Entity specs add structural validation
Datomic entity specs can validate that entities participating in a transaction satisfy predicates or contain required attributes.
Conceptually, you could enforce:
Any entity treated as a Person must have:
- :person/name
- :person/email
Entity specs validate the resulting database value during the transaction. They are useful for declarative entity-level invariants beyond basic attribute schema.
8. Transaction functions enforce domain invariants
For more complex rules, a transaction function examines db-before and returns transaction data or rejects the transaction.
For example:
A card may move to Done only if:
- it has an assignee;
- all subtasks are complete;
- its document has been published.
That cannot be expressed through :db/valueType or cardinality. It belongs in a transaction function or entity spec. Transaction functions calculate against the database state at the beginning of the transaction, and the resulting transaction is committed atomically.
The useful mental model
Datomic has three layers of enforcement:
Attribute schema
↓
Types, cardinality, uniqueness, references
Entity specs
↓
Entity shape and declarative predicates
Transaction functions
↓
Cross-entity and business invariants
Therefore, Datomic is not schemaless, but its schema is attribute-oriented rather than table-oriented.
A particularly accurate description would be:
Datomic strongly schemas facts, but only optionally schemas entity shapes.
The distributed snapshots paper you uploaded discusses consistent global states in distributed systems, but it is not directly about Datomic schema enforcement.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime