Database independence for knowledge graphs

What graph database lock-in actually costs, what portability genuinely requires, and where the honest boundary of it sits.

· 8 min read

Choosing a graph database is usually the first decision a knowledge graph project makes, and it is made at the worst possible moment — when the least is known about the workload, the data volumes, or what the graph will eventually be asked to answer. That decision then becomes very difficult to revisit, not because the data cannot be moved, but because of everything that was built on top of it in the meantime.

What lock-in actually costs

The data itself is rarely the problem. Nodes and edges export. What does not move is the accumulated work that assumed a particular engine:

  • The query surface. Every query written in Cypher, GSQL or SPARQL, in application code, notebooks, dashboards and scheduled jobs. This is usually the largest single body of affected work, and it is spread across teams that did not know they were making a portability decision.
  • The ingestion pipelines. Loaders written against one engine's bulk-import format, its transaction semantics and its particular way of handling upserts.
  • The modelling compromises. The parts of the model shaped by what the engine made cheap. Properties denormalised onto edges because traversal was expensive; a type split in two because the index behaved badly. These are invisible in a data export and are often the hardest thing to unpick, because nobody wrote down which choices were modelling and which were accommodation.
  • The operational knowledge. Tuning, backup, and the set of things the team knows not to do.

The commercial consequence is straightforward: at renewal, the cost of leaving is the cost of rebuilding all of that, and both sides know it.

What portability actually requires

“Supports multiple databases” is a claim many tools make and few mean in a load-bearing way. Real portability has three requirements, and the third is the one usually missing.

The model must be declared, not implied. If the shape of the graph exists only as the emergent consequence of ingestion scripts, there is nothing to port — you would be reverse-engineering the model from the code that happens to produce it. The schema, the relationships, the typed properties and the identity rules have to be a written artifact.

Identity must be part of the model. What makes two records the same entity is a modelling decision, not an engine feature. If identity is implemented as a database constraint, it does not survive the move; if it is declared in the contract — natural keys, composite keys, content hashes, or ordered fallbacks such as “use the DUNS number if present, otherwise the tax ID and country, otherwise the registered name and country” — it does.

Engine differences must be absorbed somewhere explicit. This is the requirement that separates genuine portability from a lowest-common-denominator abstraction. Graph engines really do differ: in how edges carry properties, in what can be indexed, in whether directionality is intrinsic, in naming rules. A portable system does not pretend those differences do not exist, and it does not force you down to the subset every engine shares. It isolates them in one declared place — in GraFlo, a database profile that names the target flavour, storage names and secondary indexes — so the model stays the model and the accommodations stay visible as accommodations.

One declarative contract — portable, credential-freeschema · identities · ingestion model · bindingsdb_profile — the only engine-aware layerstorage names · secondary indexes · flavormaterialised, unchanged modelNeo4jArangoDBTigerGraphPostgreSQLMemgraphFalkorDBNebulaGraphfile
Figure 1. Engine-specific decisions are confined to one declared layer. The contract above it does not change when the target does, which is what makes the engine choice revisable.

In GSTL that layer is a literal block in the manifest. Everything engine-shaped — storage names, secondary indexes, the target flavour — lives in it and nowhere else:

db_profile — engine specifics, quarantined
schema:
    graph:
        vertex_config:
            vertices:
            -   name: supplier
                properties:
                -   {name: duns, type: STRING}
                identity: [duns]
    db_profile:
        vertex_indexes:
            supplier:
            -   unique: false
                fields:
                -   duns

Read that as two halves. Above db_profile is a claim about the business: a supplier is identified by its DUNS number. Below it is a claim about a database: that DUNS should carry a non-unique index. Moving engines rewrites the second half and leaves the first untouched — and because the split is declared rather than conventional, you can see at review time which of your decisions were modelling and which were accommodation.

One contract, eight engines

In practice this means the graph — schema, ingestion logic and source bindings — is written once, as a single versioned artifact, and targets eight supported backends: Neo4j, ArangoDB, TigerGraph, PostgreSQL, Memgraph, FalkorDB, NebulaGraph, and a file backend that needs no database at all.

The file backend is worth a sentence of its own, because it makes the claim testable. Being able to run the model with no database running at all is how you check that nothing engine-specific crept into the contract — it is a portability test you can run in continuous integration rather than a promise you take on faith.

Consumers benefit from the same separation. Typed reads — objects of a class with filters, neighbors, bounded traversal, aggregations — are served identically across the supported engines, so applications and agents are not written against a dialect either. Changing engine does not invalidate the consumers, which is usually the larger half of the problem. That surface is the subject of governed context for AI agents.

The model as a file you own

There is a sovereignty argument underneath the technical one. Because the contract is a declarative artifact rather than a hosted configuration, it lives in your repository, is reviewed like code, and remains readable and executable without our services. Connection details are symbolic labels resolved at runtime, so credentials are never written into it.

If you stop working with us, you keep the model and it still runs. That is a deliberately unglamorous claim, and it is the one procurement should care about most — it is checkable before signing anything, by reading the artifact.

What portability covers

Portability here means the model ports and the graph is re-materialised on the new engine from the same contract and the same sources.

It is worth being precise about what that does and does not include. Transforming a populated Neo4j instance in place into a TigerGraph one is a different problem, and not what portability means here. We say so because “portable” is a word vendors stretch, and a claim you cannot check is worth less than one you can.

In practice the re-materialisation path is the one most organisations want anyway: stand the new engine up alongside the old, load it from the same contract, run both while consumers are checked against it, then cut over. The governed-change machinery that makes the contract safe to evolve across such a move is the subject of version control for world models.

Choosing an engine, later

The practical effect of separating the model from the engine is that the engine decision moves to a point where you can actually make it well — after the model exists, after the query patterns are known, and with the option of measuring candidates on your own workload rather than on a vendor's benchmark.

Some organisations will run more than one engine deliberately: an analytical store for heavy traversal, PostgreSQL where the graph is modest and the operational familiarity is worth more than graph-native performance. That is a reasonable architecture, and it is only affordable if one model can target both.

What to ask before signing

Questions worth putting to any vendor claiming portability, including us:

  • Is the model a file I hold, or configuration inside your service? Can I read it without you?
  • If I stop paying, what keeps working — and can I demonstrate that before signing rather than after?
  • Where do engine-specific decisions live? Point at them. If the answer is “the tool handles it”, they are spread through the pipelines.
  • Is identity part of the model or a database constraint? The second does not survive a move.
  • Does “supports N databases” mean the model targets them, or that the subset common to all of them does?
  • What exactly happens on a migration — is the model re-materialised, or is a live schema transformed in place? Ask for the boundary in writing.
  • Are credentials ever written into the model artifact?

The engine that implements this is GraFlo, which is open source and can be evaluated in full before any conversation with us. The glossary covers the vocabulary used here.