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
| Property | Mechanism |
|---|---|
| Atomicity | Every request is one commit — a new immutable layer containing the complete change, applied all at once. A rejected request stores nothing. |
| Consistency | When a schema exists, every commit is validated against it: types, required fields and cardinality are enforced before the commit lands. |
| Isolation | Readers see the immutable layer at commit boundaries — snapshot isolation by construction, so long-running reads never observe a half-written state. |
| Durability | Commits 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-serverDifferent 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.
// 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
- ACID Transactions — how each property is implemented internally
- Immutability — the append-only layer store that makes snapshot isolation free
- A Graph Database with Git-like Version Control — branching and merging on production data
- How to Version JSON Data — the versioning workflow step by step
- Document Insertion — full Document API reference