Suppose a team’s role is stored in a SQL row. An UPDATE replaces viewer with billing_admin, and the row now answers the most common question: what is the role today? If the application later asks who could reach the billing system last Tuesday, why the role changed, or what would become reachable after another change, the latest row has no answer. We usually add an audit table, triggers, an event log, a staging copy, or application objects for the proposed state. Each addition describes the same domain in another form that must remain faithful to the first.

Datomic approaches the problem more like a tree growing rings than a whiteboard being erased and rewritten. Hickey used this image in Deconstructing the Database: new information forms another ring around what was already known, while the earlier rings remain where they were. The current database is still easy to see, but its history has not been painted over. This begins with a smaller unit than the row.

Facts accumulate

Datomic calls an atomic fact a datom. A datom says that an entity has a value for an attribute, and records the transaction that introduced the assertion or retraction. It has five parts:

entity  attribute  value  transaction  added?

A transaction appends assertions and retractions. Current state is the set of assertions that have not later been retracted. Changing a team’s role records the end of one assertion and the beginning of another; it does not reach back and rewrite what the earlier transaction said. The entity is therefore closer to a growing dossier than a row on a preprinted form. It has a stable identity and whatever facts are known about it. Different entities can carry different attributes, references use the same representation as scalar values, and a many-valued relationship becomes several facts rather than a special container.

This fact model is useful even when the application only asks about the present. A domain can grow by adding attributes and relationships around existing identities instead of forcing every entity through one fixed row shape. Datomic gives attributes a schema for type, cardinality, uniqueness, and other constraints while allowing any entity to possess any attribute.

The database becomes a value

Every transaction adds another ring of facts. Choose a transaction basis and those rings form a complete database value: a stable view of what the database knew at that point. The live connection may advance as new transactions arrive, while a value already held by application code stays where it was captured. Holding that value does not copy all the data any more than naming an edition of an atlas copies every map. It identifies one coherent edition that queries can continue to use.

Datomic’s transaction model carries the idea into the API. with is a pure function from a database value and proposed facts to a new database value. It is like sketching the next edition in a private branch: the application can inspect the result without advancing the shared connection. transact runs the same semantic checks and makes the accepted facts durable.

The connection moves forward. A database value stays at the edition the application captured.

Change the database value, not the function

Return to the access-control example. Before granting a team a new role, the application must discover which systems become reachable and which security policies would be broken. The analysis can be an ordinary function whose first argument is a database value:

// Pseudocode. Values in, values out.
function evaluateAccess(db, team):
  return {
    reachable:  query(db, resources_reachable_by(team)),
    violations: query(db, policies_broken_by(team))
  }

current  = database(connection)
past     = asOf(current, lastQuarter)

proposed = with(current, [
  grant(team: "Contractors", role: "Billing admin")
])

evaluateAccess(current,  team: "Contractors")  // what is
evaluateAccess(past,     team: "Contractors")  // what was
evaluateAccess(proposed, team: "Contractors")  // what could be

evaluateAccess receives one coherent edition of the world. It uses the same queries whether that edition is current, historical, or hypothetical. The application can keep the current and proposed values side by side, run the same rules against each, compare the outcomes, then discard the proposal or transact its facts. An as-of value answers ordinary domain questions at a chosen point in time, while a history view opens the ledger and exposes the assertions and retractions themselves.

Current, historical, and hypothetical state share the same data model and query interface. Each can be passed directly into ordinary application functions, without a separate “preview mode.”

The whole database as input to a pure function

Functional core / imperative shell is a useful architecture for keeping I/O at the edges of a program. A deterministic core receives values and decides what should happen, while a thin shell loads inputs, persists results, and performs external effects. Tests can call the core directly without starting services or mocking its surroundings. The division becomes awkward when a decision needs broad knowledge of the domain. The core is often starved of context, fed a carefully assembled object graph, or allowed to reach through a repository and perform I/O. A database value gives the shell another option: pass one stable view of the whole database into the core, let the core derive the exact view it needs, then perform the returned transaction and effects outside it.

// Functional core
function decide(db, command):
  if not allowed(db, command):
    return rejection

  facts = factsFor(command)
  nextDb = with(db, facts)
  effects = effectsFor(nextDb, command)

  return { facts, effects }

// Imperative shell
db = database(connection)
result = decide(db, command)

transact(connection, result.facts)
send(result.effects)

The core can query the complete database value, follow relationships, use rules, and derive exactly the view it needs without performing I/O or changing durable state. Tests need no repository mock. Pass decide a different value: a minimal fixture, last Tuesday’s database, or a proposed migration.

One model for a changing world

The pieces reinforce each other. Atomic facts make entities flexible and relationships explicit. Transactions place those facts in time and preserve their provenance. Accretion turns the database into a succession of stable values. Datalog, pull, and entity views can then ask the same domain questions of any one of those values. An application may begin with the current state and flexible modeling, then later add audit views, historical reports, speculative planning, or long-running calculations without inventing a second representation of the domain.

What VevDB contributes

The model described so far comes from Datomic. VevDB adopts much of it and provides a different implementation and deployment form: a native embedded component for self-contained desktop applications, developer tools, on-premises products, and small services. The current native SDK downloads are about 3–4 MB, including the query engine and bundled SQLite storage layer.

Operational state can remain outside the permanent fact model. Caches, sessions, job state, and derived indexes often fit ordinary mutable tables, so VevDB includes a bundled SQLite API for that data. The fact database can hold what the application wants to know and remember about its domain, while SQLite handles state whose value lies in being current and replaceable.

VevDB implements datoms, data-oriented transactions, attribute schema, Datalog, pull, entities, index access, history, as-of, since, and db-with. It follows much of Datomic’s familiar model while documenting intentional differences. The implementation is native and embedded; the underlying idea remains Datomic’s: facts accumulate, the past stays available, and the database enters application code as a value.

Try the model

Open a store, transact a few facts, capture a database value, and create a hypothetical successor without changing the connection.

Get started Download VevDB Tell us what you are building