An Open-Source Graph Database with ACID Transactions

Open inAnthropic

TerminusDB is an open-source graph database — Apache 2.0 licensed — with full ACID transactions on every write. It stores linked JSON documents as a property graph, and each insert, update or delete lands as a single atomic commit: schema-validated, isolated from concurrent readers, and durable on an append-only layer store. As a bonus, the same commit machinery gives you Git-style branching, diffs and history on your data.

Why ACID matters for a graph database

Graph writes rarely touch one record — a realistic update spans several documents and the edges between them. Without atomicity, a write that fails halfway leaves dangling relationships: an order without its customer, a membership edge pointing at a deleted node. Without schema-checked consistency, invalid documents accumulate until queries return garbage. ACID is what lets you treat a multi-document graph update as one operation instead of a cleanup job.

How TerminusDB delivers each property

PropertyMechanism
AtomicityEvery request is one commit — a new immutable layer containing the complete change, applied all at once. A rejected request stores nothing.
ConsistencyWhen a schema exists, every commit is validated against it: types, required fields and cardinality are enforced before the commit lands.
IsolationReaders see the immutable layer at commit boundaries — snapshot isolation by construction, so long-running reads never observe a half-written state.
DurabilityCommits are appended to a content-addressed layer store; a failed or partial commit never modifies existing layers, so prior states stay intact and recoverable.

For the internals, see ACID Transactions — this page demonstrates the behaviour instead.

Prove atomicity in five calls

The steps below run unchanged against a local TerminusDB. The demonstration: a batch insert where one document violates the schema — the whole request is rejected and nothing is stored.

Try the examples on this page

Start TerminusDB locally:

docker run --rm -p 6363:6363 terminusdb/terminusdb-server

Different server?

1. Create a database

Loading…

Expected output:

{
  "@type": "api:DbCreateResponse",
  "api:status": "api:success"
}

2. Define a schema with a required field

A Product class where price is mandatory — this is the consistency rule the transaction will be checked against.

Loading…

Expected output:

[
  "Product"
]

3. Send a batch where one document violates the schema

Product/A is valid; Product/B is missing the required price. One request, two documents — the invalid one rejects the whole commit.

Loading…

Expected output:

{
  "@type": "api:InsertDocumentErrorResponse",
  "api:status": "api:failure",
  "api:message": "Schema check failure",
  "api:error": {
    "@type": "api:SchemaCheckFailure",
    "api:witnesses": [
      {
        "@type": "required_field_does_not_exist_in_document",
        "document": {
          "@id": "terminusdb:///data/Product/B"
        },
        "field": "terminusdb:///schema#price"
      }
    ]
  }
}

The request fails with a schema-check failure naming the offending document and field.

4. Verify nothing landed — including the valid document

This is the atomicity guarantee: the valid Product/A was not stored either.

Loading…

Expected output:

{
  "@type": "api:GetDocumentErrorResponse",
  "api:status": "api:not_found",
  "api:error": {
    "@type": "api:DocumentNotFound",
    "api:document_id": "Product/A"
  },
  "api:message": "Document not found: 'Product/A'"
}

HTTP 404 — the database is exactly as it was before the batch. No partial state, no cleanup query.

5. Retry with a valid batch — one commit, both documents

Loading…

Expected output:

[
  "Product/A",
  "Product/B"
]

Both documents now exist — written in a single commit, so a reader never sees Product/A without Product/B.

Clean up

Loading…

Same demo in TypeScript

The JavaScript client surfaces the same guarantee — a rejected addDocument throws, and the earlier state is untouched.

TYPESCRIPT
Run locally
// Schema — price is required
  await client.addDocument(
    { "@type": "Class", "@id": "Product", name: "xsd:string", price: "xsd:decimal", stock: "xsd:integer" },
    { graph_type: "schema" },
    "",
    "Add Product schema",
  )

  // One atomic request — Product/B is missing price, so the batch is rejected
  try {
    await client.addDocument(
      [
        { "@id": "Product/A", "@type": "Product", name: "Gadget A", price: 10, stock: 1 },
        { "@id": "Product/B", "@type": "Product", name: "Gadget B", stock: 2 },
      ],
      {},
      "",
      "Batch insert",
    )
  } catch (e) {
    console.log("batch rejected:", e instanceof Error ? e.message : e)
  }

  // Atomicity — the valid Product/A was not stored either
  let productA
  try {
    productA = await client.getDocument({ id: "Product/A" })
  } catch {
    productA = null
  }
  console.log("Product/A after rejected batch:", productA)

  // Fix the batch — both documents land in a single commit
  await client.addDocument(
    [
      { "@id": "Product/A", "@type": "Product", name: "Gadget A", price: 10, stock: 1 },
      { "@id": "Product/B", "@type": "Product", name: "Gadget B", price: 20, stock: 2 },
    ],
    {},
    "",
    "Batch insert (fixed)",
  )
  const products = await client.getDocument({ type: "Product", as_list: true })
  console.log(products)

What else the commit model gives you

Because every ACID commit is also a named point in history, the same machinery provides branches, time travel, a per-commit audit trail, and clone/push/pull between instances — the Git-like version control model covered in A Graph Database with Git-like Version Control.

Where it runs

TerminusDB runs as a single Docker container or a compiled binary — see Docker installation — and the source is on GitHub under Apache 2.0. For managed hosting, contact Gettier to host TerminusDB data products with the same API.

See also

Was this helpful?